Snap for 11219529 from ac76e6685f68646cb4ecac667314159b4bccf8cd to mainline-tzdata4-release

Change-Id: Id8a076dd1d4585e5ac562ffd246caab31a1f6802
diff --git a/.github/workflows/gradle_tasks_validation.yml b/.github/workflows/gradle_tasks_validation.yml
index 96b4b33..9762b9e 100644
--- a/.github/workflows/gradle_tasks_validation.yml
+++ b/.github/workflows/gradle_tasks_validation.yml
@@ -15,6 +15,23 @@
   contents: read
 
 jobs:
+  run_checkForApiChanges:
+    runs-on: ubuntu-20.04
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up JDK
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'zulu'
+          java-version: 11
+
+      - uses: gradle/gradle-build-action@v2
+
+      - name: Run checkForApiChanges
+        run: ./gradlew checkForApiChanges
+
   run_aggregateDocs:
     runs-on: ubuntu-20.04
 
@@ -30,7 +47,7 @@
       - uses: gradle/gradle-build-action@v2
 
       - name: Run aggregateDocs
-        run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew clean aggregateDocs # building the native runtime is not required for checking javadoc
+        run: ./gradlew clean aggregateDocs
 
   run_instrumentAll:
     runs-on: ubuntu-20.04
@@ -50,7 +67,7 @@
       - uses: gradle/gradle-build-action@v2
 
       - name: Run :preinstrumented:instrumentAll
-        run: SKIP_NATIVERUNTIME_BUILD=true ./gradlew :preinstrumented:instrumentAll
+        run: ./gradlew :preinstrumented:instrumentAll
 
       - name: Run :preinstrumented:instrumentAll with SDK 33
-        run: SKIP_NATIVERUNTIME_BUILD=true PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll
+        run: PREINSTRUMENTED_SDK_VERSIONS=33 ./gradlew :preinstrumented:instrumentAll
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d83e804..b47f375 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -101,7 +101,7 @@
         run: |
           TARGET="google_apis"
           echo "TARGET=$TARGET" >> $GITHUB_OUTPUT
-          
+
       - name: AVD cache
         uses: actions/cache@v3
         id: avd-cache
@@ -147,3 +147,25 @@
           path: |
             **/build/reports/*
             **/build/outputs/*/connected/*
+
+  publish-to-snapshots:
+    runs-on: ubuntu-20.04
+    env:
+      SONATYPE_LOGIN: ${{ secrets.SONATYPE_LOGIN }}
+      SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+    needs: unit-tests
+    if: github.ref == 'refs/heads/master'
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up JDK 11
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'zulu'
+          java-version: 11
+
+      - uses: gradle/gradle-build-action@v2
+
+      - name: Publish
+        run: |
+          ./gradlew publish --stacktrace --no-watch-fs
diff --git a/Android.bp b/Android.bp
index 319b5e3..2487caf 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13,7 +13,10 @@
 // limitations under the License.
 
 package {
-    default_visibility: [":__subpackages__"],
+    default_visibility: [
+        "//external/robolectric:__subpackages__",
+        "//test/robolectric-extensions:__subpackages__",
+    ],
     default_applicable_licenses: ["external_robolectric_license"],
 }
 
@@ -33,7 +36,10 @@
 // See: http://go/android-license-faq
 license {
     name: "external_robolectric_license",
-    visibility: [":__subpackages__"],
+    visibility: [
+      ":__subpackages__",
+      "//test/robolectric-extensions:__subpackages__",
+    ],
     license_kinds: [
         "SPDX-license-identifier-Apache-2.0",
         "SPDX-license-identifier-MIT",
@@ -111,6 +117,7 @@
     visibility: [
         ":__subpackages__",
         "//prebuilts/misc/common/robolectric",
+        "//test/robolectric-extensions:__subpackages__",
     ],
 }
 
@@ -133,10 +140,12 @@
     name: "Robolectric_all_upstream",
 
     static_libs: [
+        "Robolectric-aosp-plugins",
         "robolectric_meta_service_file",
         "Robolectric_shadows_httpclient_upstream",
         "Robolectric_shadows_framework_upstream",
         "Robolectric_shadows_multidex_upstream",
+        "Robolectric_shadows_versioning_upstream",
         "Robolectric_robolectric_upstream",
         "Robolectric_annotations_upstream",
         "Robolectric_resources_upstream",
diff --git a/README.md b/README.md
index 6b91ba3..b183647 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@
 
 ```groovy
 testImplementation "junit:junit:4.13.2"
-testImplementation "org.robolectric:robolectric:4.10-alpha-1"
+testImplementation "org.robolectric:robolectric:4.10.3"
 ```
 
 ## Building And Contributing
@@ -79,18 +79,3 @@
 
     ./gradlew connectedCheck
 
-### Using Snapshots
-
-If you would like to live on the bleeding edge, you can try running against a snapshot build. Keep in mind that snapshots represent the most recent changes on master and may contain bugs.
-
-#### build.gradle:
-
-```groovy
-repositories {
-    maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
-}
-
-dependencies {
-    testImplementation "org.robolectric:robolectric:4.10-SNAPSHOT"
-}
-```
diff --git a/annotations/build.gradle b/annotations/build.gradle
index 65b4f06..d8bd113 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -5,6 +5,6 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    compileOnly libs.findbugs.jsr305
     compileOnly AndroidSdk.MAX_SDK.coordinates
 }
diff --git a/build.gradle b/build.gradle
index 1eb662b..f3ab387 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,18 +7,15 @@
         google()
         mavenCentral()
         gradlePluginPortal()
-        maven {
-            url "https://plugins.gradle.org/m2/"
-        }
     }
 
     dependencies {
         gradle
-        classpath 'com.android.tools.build:gradle:7.4.2'
-        classpath 'net.ltgt.gradle:gradle-errorprone-plugin:3.0.1'
-        classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1'
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
-        classpath "com.diffplug.spotless:spotless-plugin-gradle:6.17.0"
+        classpath libs.android.gradle
+        classpath libs.error.prone.gradle
+        classpath libs.aggregate.javadocs.gradle
+        classpath libs.kotlin.gradle
+        classpath libs.spotless.gradle
     }
 }
 
@@ -120,7 +117,7 @@
     dependsOn ':aggregateJsondocs'
 }
 
-task prefetchSdks() {
+tasks.register('prefetchSdks') {
     AndroidSdk.ALL_SDKS.each { androidSdk ->
         doLast {
             println("Prefetching ${androidSdk.coordinates}...")
@@ -139,7 +136,7 @@
     }
 }
 
-task prefetchInstrumentedSdks() {
+tasks.register('prefetchInstrumentedSdks') {
     AndroidSdk.ALL_SDKS.each { androidSdk ->
         doLast {
             println("Prefetching ${androidSdk.preinstrumentedCoordinates}...")
@@ -169,7 +166,7 @@
     if (process.exitValue() != 0) System.exit(1)
 }
 
-task prefetchDependencies() {
+tasks.register('prefetchDependencies') {
     doLast {
         allprojects.each { p ->
             p.configurations.each { config ->
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 6123663..67279ef 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -11,8 +11,8 @@
     implementation gradleApi()
     implementation localGroovy()
 
-    api "com.google.guava:guava:31.1-jre"
-    api 'org.jetbrains:annotations:24.0.1'
-    implementation "org.ow2.asm:asm-tree:9.4"
-    implementation 'com.android.tools.build:gradle:7.4.2'
+    api libs.guava
+    api libs.jetbrains.annotations
+    implementation libs.asm.tree
+    implementation libs.android.gradle
 }
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 0000000..6f31e6e
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+    versionCatalogs {
+        libs {
+            from(files("../gradle/libs.versions.toml"))
+        }
+    }
+}
diff --git a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
index c0671c5..2f1476c 100644
--- a/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
+++ b/buildSrc/src/main/groovy/CheckApiChangesPlugin.groovy
@@ -28,7 +28,6 @@
             project.checkApiChanges.from.each {
                 project.dependencies.checkApiChangesFrom(it) {
                     transitive = false
-                    force = true
                 }
             }
 
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
index 324d04d..09e3b8d 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/DeployedRoboJavaModulePlugin.groovy
@@ -94,8 +94,8 @@
                     url = project.version.endsWith("-SNAPSHOT") ? snapshotsRepoUrl : releasesRepoUrl
 
                     credentials {
-                        username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
-                        password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+                        username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+                        password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
                     }
                 }
             }
diff --git a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
index 6c0e058..deb97c9 100644
--- a/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/robolectric/gradle/RoboJavaModulePlugin.groovy
@@ -13,8 +13,8 @@
         if (!skipErrorprone) {
           apply plugin: "net.ltgt.errorprone"
           project.dependencies {
-            errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
-            errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion")
+            errorprone(libs.error.prone.core)
+            errorproneJavac(libs.error.prone.javac)
           }
         }
 
diff --git a/dependencies.gradle b/dependencies.gradle
index b890dc1..204e3d3 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,40 +1,11 @@
 ext {
-    apiCompatVersion='4.9.2'
+    apiCompatVersion = libs.versions.robolectric.compat.get()
 
-    errorproneVersion='2.18.0'
-    errorproneJavacVersion='9+181-r4173-1'
-
-    // AndroidX test versions
-    axtMonitorVersion='1.6.1'
-    axtRunnerVersion='1.5.2'
-    axtRulesVersion='1.5.0'
-    axtCoreVersion='1.5.0'
-    axtTruthVersion='1.5.0'
-    espressoVersion='3.5.1'
-    axtJunitVersion='1.1.4'
-    axtTestServicesVersion='1.4.2'
-
-    // AndroidX versions
-    coreVersion='1.9.0'
-    appCompatVersion='1.6.1'
-    constraintlayoutVersion='2.1.4'
-    windowVersion='1.0.0'
-    fragmentVersion='1.5.5'
-
-    truthVersion='1.1.3'
-
-    junitVersion='4.13.2'
-
-    mockitoVersion='4.11.0'
-
-    jacocoVersion='0.8.8'
-
-    guavaJREVersion='31.1-jre'
-
-    asmVersion='9.4'
-
-    kotlinVersion='1.8.10'
-    autoServiceVersion='1.0.1'
-    multidexVersion='2.0.1'
-    sqlite4javaVersion='1.0.392'
+    // https://github.com/gradle/gradle/issues/21267
+    axtCoreVersion = libs.versions.androidx.test.core.get()
+    axtJunitVersion = libs.versions.androidx.test.ext.junit.get()
+    axtMonitorVersion = libs.versions.androidx.test.monitor.get()
+    axtRunnerVersion = libs.versions.androidx.test.runner.get()
+    axtTruthVersion = libs.versions.androidx.test.ext.truth.get()
+    espressoVersion = libs.versions.androidx.test.espresso.get()
 }
diff --git a/errorprone/build.gradle b/errorprone/build.gradle
index 1932066..5fc5616 100644
--- a/errorprone/build.gradle
+++ b/errorprone/build.gradle
@@ -20,14 +20,14 @@
     implementation project(":shadowapi")
 
     // Compile dependencies
-    implementation "com.google.errorprone:error_prone_annotation:$errorproneVersion"
-    implementation "com.google.errorprone:error_prone_refaster:$errorproneVersion"
-    implementation "com.google.errorprone:error_prone_check_api:$errorproneVersion"
-    compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
-    compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
+    implementation libs.error.prone.annotations
+    implementation libs.error.prone.refaster
+    implementation libs.error.prone.check.api
+    compileOnly libs.auto.service.annotations
+    compileOnly(AndroidSdk.MAX_SDK.coordinates)
 
-    annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
-    annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+    annotationProcessor libs.auto.service
+    annotationProcessor libs.error.prone.core
 
     // in jdk 9, tools.jar disappears!
     def toolsJar = Jvm.current().getToolsJar()
@@ -36,10 +36,10 @@
     }
 
     // Testing dependencies
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation("com.google.errorprone:error_prone_test_helpers:${errorproneVersion}") {
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation(libs.error.prone.test.helpers) {
         exclude group: 'junit', module: 'junit' // because it depends on a snapshot!?
     }
-    testCompileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
+    testCompileOnly(AndroidSdk.MAX_SDK.coordinates)
 }
diff --git a/gradle.properties b/gradle.properties
index dc9eb66..d97ed21 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-thisVersion=4.10-SNAPSHOT
+thisVersion=4.11-SNAPSHOT
 android.useAndroidX=true
 kotlin.stdlib.default.dependency=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..7b43dfa
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,237 @@
+[versions]
+robolectric-compat = "4.10.2"
+robolectric-nativeruntime-dist-compat = "1.0.1"
+
+# https://developer.android.com/studio/releases
+android-gradle = "7.4.2"
+
+# https://github.com/google/conscrypt/tags
+conscrypt = "2.5.2"
+
+# https://github.com/bcgit/bc-java/tags
+bouncycastle = "1.73"
+
+# https://github.com/findbugsproject/findbugs/tags
+findbugs-jsr305 = "3.0.2"
+
+# https://github.com/hamcrest/JavaHamcrest/releases
+hamcrest = "2.0.0.0"
+
+# https://github.com/nebula-plugins/gradle-aggregate-javadocs-plugin/releases
+aggregate-javadocs-gradle = "3.0.1"
+
+# https://github.com/google/error-prone/releases
+error-prone = "2.19.1"
+error-prone-javac = "9+181-r4173-1"
+
+# https://github.com/tbroyer/gradle-errorprone-plugin/releases
+error-prone-gradle = "3.1.0"
+
+# https://kotlinlang.org/docs/releases.html#release-details
+kotlin = "1.8.10"
+
+# https://github.com/diffplug/spotless/blob/main/CHANGES.md
+spotless-gradle = "6.18.0"
+
+# https://hc.apache.org/news.html
+apache-http-core = "4.0.1"
+apache-http-client = "4.0.3"
+
+# https://asm.ow2.io/versions.html
+asm = "9.5"
+
+# https://github.com/google/auto/releases
+auto-common = "1.2.1"
+auto-service = "1.0.1"
+auto-value = "1.10.1"
+
+compile-testing = "0.21.0"
+
+# https://github.com/google/guava/releases
+guava-jre = "31.1-jre"
+
+# https://github.com/google/gson/releases
+gson = "2.10.1"
+
+# https://github.com/google/truth/releases
+truth = "1.1.3"
+
+# https://github.com/unicode-org/icu/releases
+icu4j = "73.1"
+
+jacoco = "0.8.10"
+
+# https://github.com/javaee/javax.annotation/tags
+javax-annotation-api = "1.3.2"
+javax-annotation-jsr250-api = "1.0"
+javax-inject = "1"
+
+# https://github.com/JetBrains/java-annotations/releases
+jetbrains-annotations = "24.0.1"
+
+# https://junit.org/junit4/
+junit4 = "4.13.2"
+
+# https://github.com/google/libphonenumber/releases
+libphonenumber = "8.13.11"
+
+# https://github.com/mockito/mockito/releases
+mockito = "4.11.0"
+
+# https://github.com/mockk/mockk/releases
+mockk = "1.13.5"
+
+# https://square.github.io/okhttp/changelogs/changelog/
+okhttp = "4.11.0"
+
+# https://github.com/powermock/powermock/releases
+powermock = "2.0.9"
+
+sqlite4java = "1.0.392"
+
+# https://developer.android.com/jetpack/androidx/versions
+androidx-annotation = "1.3.0"
+androidx-appcompat = "1.6.1"
+androidx-constraintlayout = "2.1.4"
+androidx-core = "1.10.1"
+androidx-fragment = "1.5.7"
+androidx-multidex = "2.0.1"
+androidx-window = "1.0.0"
+
+# https://github.com/android/android-test/tags
+androidx-test-annotation = "1.0.1"
+androidx-test-core = "1.5.0"
+androidx-test-espresso = "3.5.1"
+androidx-test-ext-junit = "1.1.5"
+androidx-test-ext-truth = "1.5.0"
+androidx-test-monitor="1.6.1"
+androidx-test-orchestrator="1.4.2"
+androidx-test-runner = "1.5.2"
+androidx-test-services = "1.4.2"
+
+# for shadows/playservices/build.gradle
+androidx-fragment-for-shadows = "1.2.0"
+play-services-base-for-shadows = "8.4.0"
+
+[libraries]
+android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
+kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+spotless-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-gradle" }
+
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+
+auto-common = { module = "com.google.auto:auto-common", version.ref = "auto-common" }
+auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service" }
+auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" }
+auto-value-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "auto-value" }
+auto-value = { module = "com.google.auto.value:auto-value", version.ref = "auto-value" }
+
+apache-http-core = { module = "org.apache.httpcomponents:httpcore", version.ref = "apache-http-core" }
+apache-http-client = { module = "org.apache.httpcomponents:httpclient", version.ref = "apache-http-client" }
+
+asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
+asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
+asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
+asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
+
+compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" }
+
+aggregate-javadocs-gradle = { module = "com.netflix.nebula:gradle-aggregate-javadocs-plugin", version.ref = "aggregate-javadocs-gradle" }
+
+error-prone-core =  { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" }
+error-prone-annotations = { module = "com.google.errorprone:error_prone_annotation", version.ref = "error-prone" }
+error-prone-refaster= { module = "com.google.errorprone:error_prone_refaster", version.ref = "error-prone" }
+error-prone-check-api = { module = "com.google.errorprone:error_prone_check_api", version.ref = "error-prone" }
+error-prone-test-helpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "error-prone" }
+error-prone-javac =  { module = "com.google.errorprone:javac", version.ref = "error-prone-javac" }
+
+error-prone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "error-prone-gradle" }
+
+conscrypt-openjdk-uber = { module = "org.conscrypt:conscrypt-openjdk-uber", version.ref = "conscrypt" }
+bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
+findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr305" }
+
+guava = { module = "com.google.guava:guava", version.ref = "guava-jre" }
+guava-testlib = { module = "com.google.guava:guava-testlib", version.ref = "guava-jre" }
+gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
+hamcrest-junit = { module = "org.hamcrest:hamcrest-junit", version.ref = "hamcrest" }
+
+icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
+
+jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "jacoco" }
+junit4 = { module = "junit:junit", version.ref = "junit4" }
+
+javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" }
+javax-annotation-jsr250-api = { module = "javax.annotation:jsr250-api", version.ref = "javax-annotation-jsr250-api" }
+javax-inject = { module = "javax.inject:javax.inject", version.ref = "javax.inject" }
+
+jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
+
+libphonenumber = { module = "com.googlecode.libphonenumber:libphonenumber", version.ref = "libphonenumber" }
+
+okhttp = { module = "com.squareup.okhttp3:okhttp" }
+okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" }
+
+powermock-module-junit4 = { module = "org.powermock:powermock-module-junit4", version.ref = "powermock" }
+powermock-module-junit4-rule = { module = "org.powermock:powermock-module-junit4-rule", version.ref = "powermock" }
+powermock-api-mockito2 = { module = "org.powermock:powermock-api-mockito2", version.ref = "powermock" }
+powermock-classloading-xstream = { module = "org.powermock:powermock-classloading-xstream", version.ref = "powermock" }
+
+robolectric-nativeruntime-dist-compat = { module = "org.robolectric:nativeruntime-dist-compat", version.ref = "robolectric-nativeruntime-dist-compat" }
+
+sqlite4java = { module = "com.almworks.sqlite4java:sqlite4java", version.ref = "sqlite4java" }
+sqlite4java-osx = { module = "com.almworks.sqlite4java:libsqlite4java-osx", version.ref = "sqlite4java" }
+sqlite4java-linux-amd64 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-amd64", version.ref = "sqlite4java" }
+sqlite4java-win32-x64 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x64", version.ref = "sqlite4java" }
+sqlite4java-linux-i386 = { module = "com.almworks.sqlite4java:libsqlite4java-linux-i386", version.ref = "sqlite4java" }
+sqlite4java-win32-x86 = { module = "com.almworks.sqlite4java:sqlite4java-win32-x86", version.ref = "sqlite4java" }
+
+truth = { module = "com.google.truth:truth", version.ref = "truth" }
+truth-java8-extension = { module = "com.google.truth.extensions:truth-java8-extension", version.ref = "truth" }
+
+mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
+mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
+mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
+
+androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
+androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
+androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
+androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment" }
+androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" }
+androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "androidx-multidex" }
+androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }
+
+androidx-test-annotation = { module = "androidx.test:annotation", version.ref = "androidx-test-annotation" }
+androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" }
+androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test-monitor" }
+androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" }
+androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-core" }
+androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" }
+androidx-test-services = { module = "androidx.test.services:test-services", version.ref = "androidx-test-services" }
+androidx-test-services-storage = { module = "androidx.test.services:storage", version.ref = "androidx-test-services" }
+
+androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-accessibility = { module = "androidx.test.espresso:espresso-accessibility", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-remote = { module = "androidx.test.espresso:espresso-remote", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-web = { module = "androidx.test.espresso:espresso-web", version.ref = "androidx-test-espresso" }
+
+androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-idling-concurrent = { module = "androidx.test.espresso.idling:idling-concurrent", version.ref = "androidx-test-espresso" }
+androidx-test-espresso-idling-net = { module = "androidx.test.espresso.idling:idling-net", version.ref = "androidx-test-espresso" }
+
+androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" }
+androidx-test-ext-truth = { module = "androidx.test.ext:truth", version.ref = "androidx-test-ext-truth" }
+
+androidx-fragment-for-shadows = { module = "androidx.fragment:fragment", version.ref = "androidx-fragment-for-shadows" }
+play-services-base-for-shadows = { module = "com.google.android.gms:play-services-base", version.ref = "play-services-base-for-shadows" }
+play-services-basement-for-shadows = { module = "com.google.android.gms:play-services-basement", version.ref = "play-services-base-for-shadows" }
+
+[bundles]
+play-services-base-for-shadows = [ "androidx-fragment-for-shadows", "play-services-base-for-shadows", "play-services-basement-for-shadows" ]
+powermock = [ "powermock-module-junit4", "powermock-module-junit4-rule", "powermock-api-mockito2", "powermock-classloading-xstream" ]
+sqlite4java-native = [ "sqlite4java-osx", "sqlite4java-linux-amd64", "sqlite4java-win32-x64", "sqlite4java-linux-i386", "sqlite4java-win32-x86" ]
+
+[plugins]
diff --git a/integration_tests/agp/build.gradle b/integration_tests/agp/build.gradle
index a079d1e..55d9051 100644
--- a/integration_tests/agp/build.gradle
+++ b/integration_tests/agp/build.gradle
@@ -5,6 +5,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.agp'
 
     defaultConfig {
         minSdk 16
@@ -25,8 +26,8 @@
     testImplementation project(":robolectric")
     testImplementation project(":integration_tests:agp:testsupport")
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation("androidx.test:core:$axtCoreVersion")
-    testImplementation("androidx.test:runner:$axtRunnerVersion")
-    testImplementation("androidx.test.ext:junit:$axtJunitVersion")
+    testImplementation libs.junit4
+    testImplementation libs.androidx.test.core
+    testImplementation libs.androidx.test.runner
+    testImplementation libs.androidx.test.ext.junit
 }
diff --git a/integration_tests/agp/testsupport/build.gradle b/integration_tests/agp/testsupport/build.gradle
index dcec3d4..e87274f 100644
--- a/integration_tests/agp/testsupport/build.gradle
+++ b/integration_tests/agp/testsupport/build.gradle
@@ -2,6 +2,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.agp.testsupport'
 
     defaultConfig {
         minSdk 16
diff --git a/integration_tests/androidx/build.gradle b/integration_tests/androidx/build.gradle
index 96535e1..10cc8c6 100644
--- a/integration_tests/androidx/build.gradle
+++ b/integration_tests/androidx/build.gradle
@@ -5,6 +5,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.androidx'
 
     defaultConfig {
         minSdk 16
@@ -25,19 +26,19 @@
 }
 
 dependencies {
-    implementation("androidx.appcompat:appcompat:$appCompatVersion")
-    implementation("androidx.window:window:$windowVersion")
+    implementation libs.androidx.appcompat
+    implementation libs.androidx.window
 
     // Testing dependencies
     testImplementation project(path: ':testapp')
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation("androidx.test:core:$axtCoreVersion")
-    testImplementation("androidx.core:core:$coreVersion")
-    testImplementation("androidx.test:runner:$axtRunnerVersion")
-    testImplementation("androidx.test:rules:$axtRulesVersion")
-    testImplementation("androidx.test.espresso:espresso-intents:$espressoVersion")
-    testImplementation("androidx.test.ext:truth:$axtTruthVersion")
-    testImplementation("androidx.test.ext:junit:$axtJunitVersion")
-    testImplementation("com.google.truth:truth:$truthVersion")
+    testImplementation libs.junit4
+    testImplementation libs.androidx.test.core
+    testImplementation libs.androidx.core
+    testImplementation libs.androidx.test.runner
+    testImplementation libs.androidx.test.rules
+    testImplementation libs.androidx.test.espresso.intents
+    testImplementation libs.androidx.test.ext.truth
+    testImplementation libs.androidx.test.ext.junit
+    testImplementation libs.truth
 }
diff --git a/integration_tests/androidx_test/build.gradle b/integration_tests/androidx_test/build.gradle
index 7f6f621..d07ef2e 100644
--- a/integration_tests/androidx_test/build.gradle
+++ b/integration_tests/androidx_test/build.gradle
@@ -7,6 +7,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integration.axt'
 
     defaultConfig {
         minSdk 16
@@ -41,33 +42,33 @@
 }
 
 dependencies {
-    implementation "androidx.appcompat:appcompat:$appCompatVersion"
-    implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion"
-    implementation "androidx.multidex:multidex:$multidexVersion"
+    implementation libs.androidx.appcompat
+    implementation libs.androidx.constraintlayout
+    implementation libs.androidx.multidex
 
     // Testing dependencies
     testImplementation project(":robolectric")
-    testImplementation "androidx.test:runner:$axtRunnerVersion"
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "androidx.test:rules:$axtRulesVersion"
-    testImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
-    testImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
-    testImplementation "androidx.test.ext:truth:$axtTruthVersion"
-    testImplementation "androidx.test:core:$axtCoreVersion"
-    testImplementation "androidx.fragment:fragment:$fragmentVersion"
-    testImplementation "androidx.fragment:fragment-testing:$fragmentVersion"
-    testImplementation "androidx.test.ext:junit:$axtJunitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
+    testImplementation libs.androidx.test.runner
+    testImplementation libs.junit4
+    testImplementation libs.androidx.test.rules
+    testImplementation libs.androidx.test.espresso.intents
+    testImplementation libs.androidx.test.espresso.core
+    testImplementation libs.androidx.test.ext.truth
+    testImplementation libs.androidx.test.core
+    testImplementation libs.androidx.fragment
+    testImplementation libs.androidx.fragment.testing
+    testImplementation libs.androidx.test.ext.junit
+    testImplementation libs.truth
 
     androidTestImplementation project(':annotations')
-    androidTestImplementation "androidx.test:runner:$axtRunnerVersion"
-    androidTestImplementation "junit:junit:$junitVersion"
-    androidTestImplementation "androidx.test:rules:$axtRulesVersion"
-    androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
-    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
-    androidTestImplementation "androidx.test.ext:truth:$axtTruthVersion"
-    androidTestImplementation "androidx.test:core:$axtCoreVersion"
-    androidTestImplementation "androidx.test.ext:junit:$axtJunitVersion"
-    androidTestImplementation "com.google.truth:truth:$truthVersion"
-    androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion"
+    androidTestImplementation libs.androidx.test.runner
+    androidTestImplementation libs.junit4
+    androidTestImplementation libs.androidx.test.rules
+    androidTestImplementation libs.androidx.test.espresso.intents
+    androidTestImplementation libs.androidx.test.espresso.core
+    androidTestImplementation libs.androidx.test.ext.truth
+    androidTestImplementation libs.androidx.test.core
+    androidTestImplementation libs.androidx.test.ext.junit
+    androidTestImplementation libs.truth
+    androidTestUtil libs.androidx.test.services
 }
diff --git a/integration_tests/compat-target28/build.gradle b/integration_tests/compat-target28/build.gradle
index 37a8568..1fc7485 100644
--- a/integration_tests/compat-target28/build.gradle
+++ b/integration_tests/compat-target28/build.gradle
@@ -14,6 +14,7 @@
 
 android {
     compileSdk 28
+    namespace 'org.robolectric.integrationtests.compattarget28'
 
     defaultConfig {
         minSdk 16
@@ -30,10 +31,10 @@
 }
 
 dependencies {
-    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+    implementation libs.kotlin.stdlib
 
     testImplementation project(path: ':testapp')
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
+    testImplementation libs.junit4
+    testImplementation libs.truth
 }
diff --git a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
index ee56fc6..69bbf73 100644
--- a/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
+++ b/integration_tests/compat-target28/src/test/java/org/robolectric/integration/compat/target28/NormalCompatibilityTest.kt
@@ -1,7 +1,9 @@
 package org.robolectric.integration.compat.target28
 
 import android.content.Context
+import android.content.Context.VIBRATOR_SERVICE
 import android.os.Build
+import android.os.Vibrator
 import android.speech.SpeechRecognizer
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -47,4 +49,9 @@
   fun `Create speech recognizer succeed`() {
     assertThat(SpeechRecognizer.createSpeechRecognizer(application)).isNotNull()
   }
+
+  @Test
+  fun `Get default Vibrator succeed`() {
+    assertThat(application.getSystemService(VIBRATOR_SERVICE) as Vibrator).isNotNull()
+  }
 }
diff --git a/integration_tests/ctesque/build.gradle b/integration_tests/ctesque/build.gradle
index 11f27e1..3b40c88 100644
--- a/integration_tests/ctesque/build.gradle
+++ b/integration_tests/ctesque/build.gradle
@@ -7,6 +7,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.ctesque'
 
     defaultConfig {
         minSdk 16
@@ -48,24 +49,26 @@
     implementation project(':testapp')
 
     testImplementation project(':robolectric')
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation("androidx.test:monitor:$axtMonitorVersion")
-    testImplementation("androidx.test:runner:$axtRunnerVersion")
-    testImplementation("androidx.test:rules:$axtRulesVersion")
-    testImplementation("androidx.test.ext:junit:$axtJunitVersion")
-    testImplementation("androidx.test.ext:truth:$axtTruthVersion")
-    testImplementation("androidx.test:core:$axtCoreVersion")
-    testImplementation("com.google.truth:truth:${truthVersion}")
-    testImplementation("com.google.guava:guava:$guavaJREVersion")
+    testImplementation libs.junit4
+    testImplementation libs.androidx.test.monitor
+    testImplementation libs.androidx.test.runner
+    testImplementation libs.androidx.test.rules
+    testImplementation libs.androidx.test.ext.junit
+    testImplementation libs.androidx.test.ext.truth
+    testImplementation libs.androidx.test.core
+    testImplementation libs.androidx.test.espresso.core
+    testImplementation libs.truth
+    testImplementation libs.guava
 
     // Testing dependencies
     androidTestImplementation project(':shadowapi')
-    androidTestImplementation("androidx.test:monitor:$axtMonitorVersion")
-    androidTestImplementation("androidx.test:runner:$axtRunnerVersion")
-    androidTestImplementation("androidx.test:rules:$axtRulesVersion")
-    androidTestImplementation("androidx.test.ext:junit:$axtJunitVersion")
-    androidTestImplementation("androidx.test.ext:truth:$axtTruthVersion")
-    androidTestImplementation("com.google.truth:truth:${truthVersion}")
-    androidTestImplementation("com.google.guava:guava:$guavaJREVersion")
-    androidTestUtil "androidx.test.services:test-services:$axtTestServicesVersion"
+    androidTestImplementation libs.androidx.test.monitor
+    androidTestImplementation libs.androidx.test.runner
+    androidTestImplementation libs.androidx.test.rules
+    androidTestImplementation libs.androidx.test.ext.junit
+    androidTestImplementation libs.androidx.test.ext.truth
+    androidTestImplementation libs.androidx.test.espresso.core
+    androidTestImplementation libs.truth
+    androidTestImplementation libs.guava
+    androidTestUtil libs.androidx.test.services
 }
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
index c1922b6..5e4de01 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/content/res/ResourcesTest.java
@@ -503,6 +503,14 @@
     assertThat(id).isEqualTo(0);
   }
 
+  @Test
+  @SdkSuppress(minSdkVersion = LOLLIPOP)
+  @Config(minSdk = LOLLIPOP)
+  public void getIdentifier_material() {
+    int id = Resources.getSystem().getIdentifier("btn_check_material_anim", "drawable", "android");
+    assertThat(id).isGreaterThan(0);
+  }
+
   /**
    * Public framework symbols are defined here:
    * https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/public.xml
diff --git a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
index fa11ce7..eea5dea 100644
--- a/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
+++ b/integration_tests/ctesque/src/sharedTest/java/android/database/SQLiteDatabaseTest.java
@@ -12,6 +12,7 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.Suppress;
 import com.google.common.base.Ascii;
 import com.google.common.base.Throwables;
 import com.google.common.io.ByteStreams;
@@ -174,7 +175,8 @@
   }
 
   // TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android.
-  @SdkSuppress(minSdkVersion = 34)
+  // Use Suppress here to stop it from running on emulators, but not on Robolectric
+  @Suppress
   @Test
   public void cursorWindow_finalize_concurrentStressTest() throws Throwable {
     final PrintStream originalErr = System.err;
@@ -223,4 +225,18 @@
     c.close();
     assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder();
   }
+
+  @Test
+  @Config(minSdk = LOLLIPOP)
+  @SdkSuppress(minSdkVersion = LOLLIPOP)
+  public void regex_selection() {
+    ContentValues values = new ContentValues();
+    values.put("first_column", "test");
+    database.insert("table_name", null, values);
+    String select = "first_column regexp ?";
+    String[] selectArgs = {
+      "test",
+    };
+    assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1);
+  }
 }
diff --git a/integration_tests/dependency-on-stubs/build.gradle b/integration_tests/dependency-on-stubs/build.gradle
index 6efe513..683de18 100644
--- a/integration_tests/dependency-on-stubs/build.gradle
+++ b/integration_tests/dependency-on-stubs/build.gradle
@@ -6,13 +6,13 @@
 
 dependencies {
     api project(":robolectric")
-    api "junit:junit:${junitVersion}"
+    api libs.junit4
 
     testImplementation files("${System.getenv("ANDROID_HOME")}/platforms/android-29/android.jar")
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-    testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0"
+    testImplementation libs.truth
+    testImplementation libs.mockito
+    testImplementation libs.hamcrest.junit
 }
diff --git a/integration_tests/jacoco-offline/build.gradle b/integration_tests/jacoco-offline/build.gradle
index e5d3bb5..3db34c0 100644
--- a/integration_tests/jacoco-offline/build.gradle
+++ b/integration_tests/jacoco-offline/build.gradle
@@ -3,6 +3,8 @@
 apply plugin: RoboJavaModulePlugin
 apply plugin: "jacoco"
 
+def jacocoVersion = libs.versions.jacoco.get()
+
 jacoco {
     toolVersion = jacocoVersion
 }
@@ -18,7 +20,7 @@
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
 
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
+    testImplementation libs.junit4
     testImplementation "org.jacoco:org.jacoco.agent:$jacocoVersion:runtime"
 }
 
diff --git a/integration_tests/kotlin/build.gradle b/integration_tests/kotlin/build.gradle
index 68c5c67..fd52d97 100644
--- a/integration_tests/kotlin/build.gradle
+++ b/integration_tests/kotlin/build.gradle
@@ -21,8 +21,8 @@
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
+    testImplementation libs.kotlin.stdlib
+    testImplementation libs.junit4
+    testImplementation libs.truth
     testImplementation "androidx.test:core:$axtCoreVersion@aar"
 }
diff --git a/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt
new file mode 100644
index 0000000..6d77aa9
--- /dev/null
+++ b/integration_tests/kotlin/src/test/kotlin/org/robolectric/integrationtests/kotlin/ParameterizedRobolectricTestRunnerTest.kt
@@ -0,0 +1,28 @@
+package org.robolectric.integrationtests.kotlin
+
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+import org.robolectric.annotation.Config
+
+@RunWith(ParameterizedRobolectricTestRunner::class)
+class ParameterizedRobolectricTestRunnerTest(private var uri: Uri) {
+  @Test
+  @Config(manifest = Config.NONE)
+  fun parse() {
+    val currentUri = Uri.parse("http://host/")
+    assertThat(currentUri).isEqualTo(uri)
+  }
+
+  companion object {
+    @Parameters
+    @JvmStatic
+    fun getTestData(): Collection<*> {
+      val data = arrayOf<Any>(Uri.parse("http://host/"))
+      return listOf(data)
+    }
+  }
+}
diff --git a/integration_tests/libphonenumber/build.gradle b/integration_tests/libphonenumber/build.gradle
index 2c27a79..61120f2 100644
--- a/integration_tests/libphonenumber/build.gradle
+++ b/integration_tests/libphonenumber/build.gradle
@@ -4,10 +4,10 @@
 
 dependencies {
     api project(":robolectric")
-    api "junit:junit:${junitVersion}"
+    api libs.junit4
     compileOnly AndroidSdk.MAX_SDK.coordinates
 
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation 'com.googlecode.libphonenumber:libphonenumber:8.13.8'
-}
\ No newline at end of file
+    testImplementation libs.truth
+    testImplementation libs.libphonenumber
+}
diff --git a/integration_tests/memoryleaks/build.gradle b/integration_tests/memoryleaks/build.gradle
index 2cc5124..91c5eb0 100644
--- a/integration_tests/memoryleaks/build.gradle
+++ b/integration_tests/memoryleaks/build.gradle
@@ -5,6 +5,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.memoryleaks'
 
     defaultConfig {
         minSdk 16
@@ -28,7 +29,7 @@
     // Testing dependencies
     testImplementation project(path: ':testapp')
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.guava:guava-testlib:$guavaJREVersion"
-    testImplementation "androidx.fragment:fragment:$fragmentVersion"
+    testImplementation libs.junit4
+    testImplementation libs.guava.testlib
+    testImplementation libs.androidx.fragment
 }
diff --git a/integration_tests/mockito-experimental/build.gradle b/integration_tests/mockito-experimental/build.gradle
index 4aafcbc..f5172d6 100644
--- a/integration_tests/mockito-experimental/build.gradle
+++ b/integration_tests/mockito-experimental/build.gradle
@@ -8,7 +8,7 @@
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito.inline
 }
diff --git a/integration_tests/mockito-kotlin/build.gradle b/integration_tests/mockito-kotlin/build.gradle
index ae97f1b..776f33b 100644
--- a/integration_tests/mockito-kotlin/build.gradle
+++ b/integration_tests/mockito-kotlin/build.gradle
@@ -18,8 +18,8 @@
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
     testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
-    testImplementation "org.mockito:mockito-core:$mockitoVersion"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.kotlin.stdlib
+    testImplementation libs.mockito
 }
diff --git a/integration_tests/mockito/build.gradle b/integration_tests/mockito/build.gradle
index e199cd7..31e6ce6 100644
--- a/integration_tests/mockito/build.gradle
+++ b/integration_tests/mockito/build.gradle
@@ -8,7 +8,7 @@
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-}
\ No newline at end of file
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
+}
diff --git a/integration_tests/mockk/build.gradle b/integration_tests/mockk/build.gradle
index 78344a9..1d59071 100644
--- a/integration_tests/mockk/build.gradle
+++ b/integration_tests/mockk/build.gradle
@@ -21,7 +21,7 @@
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation 'io.mockk:mockk:1.13.4'
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockk
 }
diff --git a/integration_tests/nativegraphics/build.gradle b/integration_tests/nativegraphics/build.gradle
index 10e8f61..f88f53a 100644
--- a/integration_tests/nativegraphics/build.gradle
+++ b/integration_tests/nativegraphics/build.gradle
@@ -7,6 +7,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.integrationtests.nativegraphics'
 
     defaultConfig {
         minSdk 26
@@ -32,9 +33,9 @@
     testImplementation AndroidSdk.MAX_SDK.coordinates
     testImplementation project(':robolectric')
 
-    testImplementation "androidx.core:core:$coreVersion"
-    testImplementation "androidx.test.ext:junit:$axtJunitVersion"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.androidx.core
+    testImplementation libs.androidx.test.ext.junit
+    testImplementation libs.truth
+    testImplementation libs.junit4
+    testImplementation libs.mockito
 }
diff --git a/integration_tests/play_services/build.gradle b/integration_tests/play_services/build.gradle
index f7499dd..0e05dd6 100644
--- a/integration_tests/play_services/build.gradle
+++ b/integration_tests/play_services/build.gradle
@@ -9,7 +9,7 @@
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
+    testImplementation libs.junit4
+    testImplementation libs.truth
     testImplementation "com.google.android.gms:play-services-basement:18.0.1"
-}
\ No newline at end of file
+}
diff --git a/integration_tests/powermock/build.gradle b/integration_tests/powermock/build.gradle
index be4180c..6d5cf68 100644
--- a/integration_tests/powermock/build.gradle
+++ b/integration_tests/powermock/build.gradle
@@ -7,11 +7,8 @@
     compileOnly AndroidSdk.MAX_SDK.coordinates
 
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
 
-    testImplementation "org.powermock:powermock-module-junit4:2.0.9"
-    testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9"
-    testImplementation "org.powermock:powermock-api-mockito2:2.0.9"
-    testImplementation "org.powermock:powermock-classloading-xstream:2.0.9"
-}
\ No newline at end of file
+    testImplementation libs.bundles.powermock
+}
diff --git a/integration_tests/security-providers/build.gradle b/integration_tests/security-providers/build.gradle
index f96df56..8ac0264 100644
--- a/integration_tests/security-providers/build.gradle
+++ b/integration_tests/security-providers/build.gradle
@@ -4,12 +4,12 @@
 
 dependencies {
     api project(":robolectric")
-    api "junit:junit:${junitVersion}"
+    api libs.junit4
     compileOnly AndroidSdk.MAX_SDK.coordinates
 
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.4.0"
-    testImplementation "com.squareup.okhttp3:okhttp"
-    testImplementation platform("com.squareup.okhttp3:okhttp-bom:4.10.0")
+    testImplementation libs.truth
+    testImplementation libs.conscrypt.openjdk.uber
+    testImplementation libs.okhttp
+    testImplementation platform(libs.okhttp.bom)
 }
diff --git a/integration_tests/sparsearray/build.gradle b/integration_tests/sparsearray/build.gradle
index 7627177..1e4ba1d 100644
--- a/integration_tests/sparsearray/build.gradle
+++ b/integration_tests/sparsearray/build.gradle
@@ -14,6 +14,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.sparsearray'
 
     defaultConfig {
         minSdk 16
@@ -41,7 +42,7 @@
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.kotlin.stdlib
 }
diff --git a/junit/build.gradle b/junit/build.gradle
index 9a11978..5d58552 100644
--- a/junit/build.gradle
+++ b/junit/build.gradle
@@ -11,6 +11,6 @@
     api project(":shadowapi")
     api project(":utils:reflector")
 
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
-    compileOnly "junit:junit:${junitVersion}"
+    compileOnly libs.findbugs.jsr305
+    compileOnly libs.junit4
 }
diff --git a/nativeruntime/build.gradle b/nativeruntime/build.gradle
index 1ef9317..e784984 100644
--- a/nativeruntime/build.gradle
+++ b/nativeruntime/build.gradle
@@ -49,8 +49,8 @@
         url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
 
         credentials {
-          username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
-          password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+          username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+          password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
         }
       }
     }
@@ -64,17 +64,17 @@
 dependencies {
   api project(":utils")
   api project(":utils:reflector")
-  api "com.google.guava:guava:$guavaJREVersion"
+  api libs.guava
 
-  implementation "org.robolectric:nativeruntime-dist-compat:1.0.0"
+  implementation libs.robolectric.nativeruntime.dist.compat
 
-  annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
-  compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
+  annotationProcessor libs.auto.service
+  compileOnly libs.auto.service.annotations
   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}"
+  testImplementation libs.junit4
+  testImplementation libs.truth
 }
diff --git a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
index 0391159..e5d395f 100644
--- a/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
+++ b/nativeruntime/src/test/java/org/robolectric/nativeruntime/DefaultNativeRuntimeLoaderTest.java
@@ -2,7 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.O;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
 
 import android.database.CursorWindow;
 import android.database.sqlite.SQLiteDatabase;
@@ -33,8 +33,8 @@
 
   @Test
   public void extracts_fontsAndIcuData() {
-    assumeTrue(hasResource("fonts"));
-    assumeTrue(hasResource("icu/icudt68l.dat"));
+    assume().that(hasResource("fonts")).isTrue();
+    assume().that(hasResource("icu/icudt68l.dat")).isTrue();
     DefaultNativeRuntimeLoader defaultNativeRuntimeLoader = new DefaultNativeRuntimeLoader();
     defaultNativeRuntimeLoader.ensureLoaded();
     // Check that extraction of some key files worked.
diff --git a/pluginapi/build.gradle b/pluginapi/build.gradle
index 375cd10..9d78852 100644
--- a/pluginapi/build.gradle
+++ b/pluginapi/build.gradle
@@ -5,11 +5,11 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+    compileOnly libs.findbugs.jsr305
     api project(":annotations")
-    api "com.google.guava:guava:$guavaJREVersion"
+    api libs.guava
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
 }
diff --git a/plugins/maven-dependency-resolver/build.gradle b/plugins/maven-dependency-resolver/build.gradle
index de20b2b..2aa33d9 100644
--- a/plugins/maven-dependency-resolver/build.gradle
+++ b/plugins/maven-dependency-resolver/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
 import org.robolectric.gradle.DeployedRoboJavaModulePlugin
 import org.robolectric.gradle.RoboJavaModulePlugin
 
@@ -13,7 +14,7 @@
     }
 }
 
-tasks.withType(GenerateModuleMetadata) {
+tasks.withType(GenerateModuleMetadata).configureEach {
     // We don't want to release gradle module metadata now to avoid
     // potential compatibility problems.
     enabled = false
@@ -22,11 +23,11 @@
 compileKotlin {
     // Use java/main classes directory to replace default kotlin/main to
     // avoid d8 error when dexing & desugaring kotlin classes with non-exist
-    // kotlin/main directory because utils module doesn't have kotlin code
+    // kotlin/main directory because this module doesn't have kotlin code
     // in production. If utils module starts to add Kotlin code in main source
     // set, we can remove this destinationDirectory modification.
     destinationDirectory = file("${projectDir}/build/classes/java/main")
-    compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+    compilerOptions.jvmTarget = JvmTarget.JVM_1_8
 }
 
 afterEvaluate {
@@ -48,10 +49,12 @@
 dependencies {
     api project(":pluginapi")
     api project(":utils")
-    api "com.google.guava:guava:$guavaJREVersion"
+    api libs.auto.value.annotations
+    api libs.guava
+    annotationProcessor libs.auto.value
 
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "org.mockito:mockito-core:$mockitoVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+    testImplementation libs.junit4
+    testImplementation libs.mockito
+    testImplementation libs.truth
+    testImplementation libs.kotlin.stdlib
 }
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 60f852d..adeda9e 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
@@ -2,6 +2,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
@@ -24,6 +25,7 @@
 import java.util.Base64;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import javax.annotation.Nonnull;
 import org.robolectric.util.Logger;
 
 /**
@@ -82,15 +84,27 @@
                   return Futures.immediateFuture(null);
                 }
                 createArtifactSubdirectory(artifact, localRepositoryDir);
-                boolean pomValid =
+                ValidationResult pomResult =
                     validateStagedFiles(artifact.pomPath(), artifact.pomSha512Path());
-                if (!pomValid) {
-                  throw new AssertionError("SHA512 mismatch for POM file fetched in " + artifact);
+                if (!pomResult.isSuccess()) {
+                  throw new AssertionError(
+                      "SHA-512 mismatch for POM file for "
+                          + artifact
+                          + ", expected SHA-512="
+                          + pomResult.expectedHashCode()
+                          + ", actual SHA-512="
+                          + pomResult.calculatedHashCode());
                 }
-                boolean jarValid =
+                ValidationResult jarResult =
                     validateStagedFiles(artifact.jarPath(), artifact.jarSha512Path());
-                if (!jarValid) {
-                  throw new AssertionError("SHA512 mismatch for JAR file fetched in " + artifact);
+                if (!jarResult.isSuccess()) {
+                  throw new AssertionError(
+                      "SHA-512 mismatch for POM file for "
+                          + artifact
+                          + ", expected SHA-512="
+                          + jarResult.expectedHashCode()
+                          + ", actual SHA-512="
+                          + jarResult.calculatedHashCode());
                 }
                 Logger.info(
                     String.format(
@@ -123,7 +137,8 @@
     new File(repositoryDir, artifact.pomSha512Path()).delete();
   }
 
-  private boolean validateStagedFiles(String filePath, String sha512Path) throws IOException {
+  private ValidationResult validateStagedFiles(String filePath, String sha512Path)
+      throws IOException {
     File tempFile = new File(this.stagingRepositoryDir, filePath);
     File sha512File = new File(this.stagingRepositoryDir, sha512Path);
 
@@ -131,7 +146,24 @@
         HashCode.fromString(new String(Files.asByteSource(sha512File).read(), UTF_8));
 
     HashCode actual = Files.asByteSource(tempFile).hash(Hashing.sha512());
-    return expected.equals(actual);
+    return ValidationResult.create(expected.equals(actual), expected.toString(), actual.toString());
+  }
+
+  @AutoValue
+  abstract static class ValidationResult {
+    abstract boolean isSuccess();
+
+    @Nonnull
+    abstract String expectedHashCode();
+
+    @Nonnull
+    abstract String calculatedHashCode();
+
+    static ValidationResult create(
+        boolean isSuccess, String expectedHashCode, String calculatedHashCode) {
+      return new AutoValue_MavenArtifactFetcher_ValidationResult(
+          isSuccess, expectedHashCode, calculatedHashCode);
+    }
   }
 
   private void createArtifactSubdirectory(MavenJarArtifact artifact, File repositoryDir)
@@ -218,6 +250,9 @@
       try (InputStream inputStream = connection.getInputStream();
           FileOutputStream outputStream = new FileOutputStream(localFile)) {
         ByteStreams.copy(inputStream, outputStream);
+        // Ensure all contents are written to disk.
+        outputStream.flush();
+        outputStream.getFD().sync();
       }
       return Futures.immediateFuture(null);
     }
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
old mode 100755
new mode 100644
diff --git a/preinstrumented/build.gradle b/preinstrumented/build.gradle
index 95d533e..8ffb5bb 100644
--- a/preinstrumented/build.gradle
+++ b/preinstrumented/build.gradle
@@ -17,11 +17,12 @@
 }
 
 dependencies {
-    implementation "com.google.guava:guava:$guavaJREVersion"
+    implementation libs.guava
     implementation project(":sandbox")
+    implementation project(":shadows:versioning")
 }
 
-task instrumentAll {
+tasks.register('instrumentAll') {
     dependsOn ':prefetchSdks'
     dependsOn 'build'
 
@@ -42,11 +43,11 @@
     }
 }
 
-task('sourcesJar', type: Jar) {
+tasks.register('sourcesJar', Jar) {
     archiveClassifier = "sources"
 }
 
-task('javadocJar', type: Jar) {
+tasks.register('javadocJar', Jar) {
     archiveClassifier = "javadoc"
 }
 
@@ -102,8 +103,8 @@
                 url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
 
                 credentials {
-                    username = System.properties["sonatype-login"] ?: System.env['sonatypeLogin']
-                    password = System.properties["sonatype-password"] ?: System.env['sonatypePassword']
+                    username = System.properties["sonatype-login"] ?: System.env['SONATYPE_LOGIN']
+                    password = System.properties["sonatype-password"] ?: System.env['SONATYPE_PASSWORD']
                 }
             }
         }
@@ -132,4 +133,4 @@
     AndroidSdk.ALL_SDKS.each { androidSdk ->
         delete "${buildDir}/${androidSdk.preinstrumentedJarFileName}"
     }
-}
\ No newline at end of file
+}
diff --git a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
index a9c5ace..753c167 100644
--- a/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
+++ b/preinstrumented/src/main/java/org/robolectric/preinstrumented/JarInstrumentor.java
@@ -8,7 +8,6 @@
 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;
@@ -21,6 +20,8 @@
 import org.robolectric.internal.bytecode.InstrumentationConfiguration;
 import org.robolectric.internal.bytecode.Interceptors;
 import org.robolectric.util.inject.Injector;
+import org.robolectric.versioning.AndroidVersionInitTools;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
 
 /** Runs Robolectric invokedynamic instrumentation on an android-all jar. */
 public class JarInstrumentor {
@@ -146,14 +147,7 @@
   }
 
   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"));
+    AndroidRelease release = AndroidVersionInitTools.computeReleaseVersion(jarFile);
+    return release.getSdkInt();
   }
 }
diff --git a/processor/build.gradle b/processor/build.gradle
index ac14e42..9185d08 100644
--- a/processor/build.gradle
+++ b/processor/build.gradle
@@ -35,21 +35,21 @@
     api project(":annotations")
     api project(":shadowapi")
 
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
-    api "org.ow2.asm:asm:${asmVersion}"
-    api "org.ow2.asm:asm-commons:${asmVersion}"
-    api "com.google.guava:guava:$guavaJREVersion"
-    api "com.google.code.gson:gson:2.10.1"
-    implementation 'com.google.auto:auto-common:1.1.2'
+    compileOnly libs.findbugs.jsr305
+    api libs.asm
+    api libs.asm.commons
+    api libs.guava
+    api libs.gson
+    implementation libs.auto.common
 
     def toolsJar = Jvm.current().getToolsJar()
     if (toolsJar != null) {
         implementation files(toolsJar)
     }
 
-    testImplementation "javax.annotation:jsr250-api:1.0"
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-    testImplementation "com.google.testing.compile:compile-testing:0.21.0"
-    testImplementation "com.google.truth:truth:${truthVersion}"
+    testImplementation libs.javax.annotation.jsr250.api
+    testImplementation libs.junit4
+    testImplementation libs.mockito
+    testImplementation libs.compile.testing
+    testImplementation libs.truth
 }
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 fd5e77d..d9905f6 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/RobolectricModel.java
@@ -1,5 +1,6 @@
 package org.robolectric.annotation.processing;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.Maps.newHashMap;
 import static com.google.common.collect.Maps.newTreeMap;
 import static com.google.common.collect.Sets.newTreeSet;
@@ -127,6 +128,10 @@
     }
 
     public void addResetter(TypeElement shadowTypeElement, ExecutableElement elem) {
+      checkState(
+          !resetterMap.containsKey(shadowTypeElement.getQualifiedName().toString()),
+          "Trying to register a duplicate resetter on %s",
+          shadowTypeElement.getQualifiedName());
       registerType(shadowTypeElement);
 
       resetterMap.put(shadowTypeElement.getQualifiedName().toString(),
diff --git a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
index d409f83..39df257 100644
--- a/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
+++ b/processor/src/main/java/org/robolectric/annotation/processing/validator/ResetterValidator.java
@@ -1,6 +1,9 @@
 package org.robolectric.annotation.processing.validator;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.ExecutableElement;
@@ -13,6 +16,9 @@
  * Validator that checks usages of {@link org.robolectric.annotation.Resetter}.
  */
 public class ResetterValidator extends FoundOnImplementsValidator {
+
+  private final Map<TypeElement, ExecutableElement> resetterMethodsByClass = new HashMap<>();
+
   public ResetterValidator(RobolectricModel.Builder modelBuilder, ProcessingEnvironment env) {
     super(modelBuilder, env, "org.robolectric.annotation.Resetter");
   }
@@ -35,7 +41,19 @@
         error("@Resetter methods must not have parameters");
         error = true;
       }
+      if (resetterMethodsByClass.containsKey(parent)) {
+        error(
+            String.format(
+                Locale.US,
+                "Duplicate @Resetter methods found on %s: %s() and %s(). Only one @Resetter method"
+                    + " is permitted on each shadow.",
+                parent.getQualifiedName(),
+                resetterMethodsByClass.get(parent).getSimpleName(),
+                elem.getSimpleName()));
+        error = true;
+      }
       if (!error) {
+        resetterMethodsByClass.put(parent, elem);
         modelBuilder.addResetter(parent, elem);
       }
     }
diff --git a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
index 6890040..c7924e8 100644
--- a/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
+++ b/processor/src/test/java/org/robolectric/annotation/processing/validator/ResetterValidatorTest.java
@@ -12,7 +12,8 @@
 public class ResetterValidatorTest {
   @Test
   public void resetterWithoutImplements_shouldNotCompile() {
-    final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
+    final String testClass =
+        "org.robolectric.annotation.processing.shadows.ShadowResetterWithoutImplements";
     assertAbout(singleClass())
       .that(testClass)
       .failsToCompile()
@@ -22,7 +23,8 @@
 
   @Test
   public void nonStaticResetter_shouldNotCompile() {
-    final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
+    final String testClass =
+        "org.robolectric.annotation.processing.shadows.ShadowResetterNonStatic";
     assertAbout(singleClass())
       .that(testClass)
       .failsToCompile()
@@ -32,7 +34,8 @@
 
   @Test
   public void nonPublicResetter_shouldNotCompile() {
-    final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
+    final String testClass =
+        "org.robolectric.annotation.processing.shadows.ShadowResetterNonPublic";
     assertAbout(singleClass())
       .that(testClass)
       .failsToCompile()
@@ -42,7 +45,8 @@
 
   @Test
   public void resetterWithParameters_shouldNotCompile() {
-    final String testClass = "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
+    final String testClass =
+        "org.robolectric.annotation.processing.shadows.ShadowResetterWithParameters";
     assertAbout(singleClass())
       .that(testClass)
       .failsToCompile()
@@ -51,6 +55,20 @@
   }
 
   @Test
+  public void twoValidResetters_shouldNotCompile() {
+    final String testClass = "org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters";
+
+    assertAbout(singleClass())
+        .that(testClass)
+        .failsToCompile()
+        .withErrorContaining(
+            "Duplicate @Resetter methods found on"
+                + " org.robolectric.annotation.processing.shadows.ShadowWithTwoResetters:"
+                + " resetter_method_one() and resetter_method_two().")
+        .onLine(13);
+  }
+
+  @Test
   public void goodResetter_shouldCompile() {
     final String testClass = "org.robolectric.annotation.processing.shadows.ShadowDummy";
     assertAbout(singleClass())
diff --git a/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
new file mode 100644
index 0000000..8183073
--- /dev/null
+++ b/processor/src/test/resources/org/robolectric/annotation/processing/shadows/ShadowWithTwoResetters.java
@@ -0,0 +1,15 @@
+package org.robolectric.annotation.processing.shadows;
+
+import com.example.objects.Dummy;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@Implements(Dummy.class)
+public class ShadowWithTwoResetters {
+
+  @Resetter
+  public static void resetter_method_one() {}
+
+  @Resetter
+  public static void resetter_method_two() {}
+}
diff --git a/resources/build.gradle b/resources/build.gradle
index 129dc20..077cdd3 100644
--- a/resources/build.gradle
+++ b/resources/build.gradle
@@ -9,11 +9,11 @@
     api project(":annotations")
     api project(":pluginapi")
 
-    api "com.google.guava:guava:$guavaJREVersion"
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    api libs.guava
+    compileOnly libs.findbugs.jsr305
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "com.google.testing.compile:compile-testing:0.21.0"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.compile.testing
+    testImplementation libs.mockito
 }
diff --git a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
index cfabcbb..7402b49 100644
--- a/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
+++ b/resources/src/main/java/org/robolectric/manifest/AndroidManifest.java
@@ -53,6 +53,7 @@
   private String processName;
   private String themeRef;
   private String labelRef;
+  private String appComponentFactory; // Added from SDK 28
   private Integer minSdkVersion;
   private Integer targetSdkVersion;
   private Integer maxSdkVersion;
@@ -191,6 +192,7 @@
         rClassName = packageName + ".R";
 
         Node applicationNode = findApplicationNode(manifestDocument);
+        // Parse application node of the AndroidManifest.xml
         if (applicationNode != null) {
           NamedNodeMap attributes = applicationNode.getAttributes();
           int attrCount = attributes.getLength();
@@ -204,6 +206,7 @@
           processName = applicationAttributes.get("android:process");
           themeRef = applicationAttributes.get("android:theme");
           labelRef = applicationAttributes.get("android:label");
+          appComponentFactory = applicationAttributes.get("android:appComponentFactory");
 
           parseReceivers(applicationNode);
           parseServices(applicationNode);
@@ -605,6 +608,11 @@
     return labelRef;
   }
 
+  public String getAppComponentFactory() {
+    parseAndroidManifest();
+    return appComponentFactory;
+  }
+
   /**
    * Returns the minimum Android SDK version that this package expects to be runnable on, as
    * specified in the manifest.
diff --git a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
index 0de3390..8b8165b 100644
--- a/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
+++ b/resources/src/main/java/org/robolectric/res/android/LoadedArsc.java
@@ -526,32 +526,79 @@
         return 0;
       }
 
+      // for (const auto& type_entry : type_spec->type_entries) {
       for (ResTable_type iter : type_spec.types) {
+        // const incfs::verified_map_ptr<ResTable_type>& type = type_entry.type;
         ResTable_type type = iter;
+        // const size_t entry_count = dtohl(type->entryCount);
         int entry_count = type.entryCount;
+        // const auto entry_offsets = type.offset(dtohs(type->header.headerSize));
 
+        // for (size_t entry_idx = 0; entry_idx < entry_count; entry_idx++) {
         for (int entry_idx = 0; entry_idx < entry_count; entry_idx++) {
-          // const uint32_t* entry_offsets = reinterpret_cast<const uint32_t*>(
-          //     reinterpret_cast<const uint8_t*>(type.type) + dtohs(type.type.header.headerSize));
-          // ResTable_type entry_offsets = new ResTable_type(type.myBuf(),
-          //     type.myOffset() + type.header.headerSize);
-          // int offset = dtohl(entry_offsets[entry_idx]);
-          int offset = dtohl(type.entryOffset(entry_idx));
+          // uint32_t offset;
+          int offset;
+          // uint16_t res_idx;
+          short res_idx;
+          // if (type->flags & ResTable_type::FLAG_SPARSE) {
+          if (isTruthy(type.flags & ResTable_type.FLAG_SPARSE)) {
+            // auto sparse_entry = entry_offsets.convert<ResTable_sparseTypeEntry>() + entry_idx;
+
+            ResTable_sparseTypeEntry sparse_entry =
+                new ResTable_sparseTypeEntry(
+                    type.myBuf(), type.myOffset() + entry_idx * ResTable_sparseTypeEntry.SIZEOF);
+            // if (!sparse_entry) {
+            //   return base::unexpected(IOError::PAGES_MISSING);
+            // }
+            // TODO: implement above
+            // offset = dtohs(sparse_entry->offset) * 4u;
+            offset = dtohs(sparse_entry.offset) * 4;
+            // res_idx  = dtohs(sparse_entry->idx);
+            res_idx = dtohs(sparse_entry.idx);
+            // } else if (type->flags & ResTable_type::FLAG_OFFSET16) {
+          } else if (isTruthy(type.flags & ResTable_type.FLAG_OFFSET16)) {
+            // auto entry = entry_offsets.convert<uint16_t>() + entry_idx;
+            int entry = type.entryOffset(entry_idx);
+            // if (!entry) {
+            //   return base::unexpected(IOError::PAGES_MISSING);
+            // }
+            // offset = offset_from16(entry.value());
+            offset = entry;
+            // res_idx = entry_idx;
+            res_idx = (short) entry_idx;
+          } else {
+            // auto entry = entry_offsets.convert<uint32_t>() + entry_idx;
+            int entry = type.entryOffset(entry_idx);
+            // if (!entry) {
+            //   return base::unexpected(IOError::PAGES_MISSING);
+            // }
+            // offset = dtohl(entry.value());
+            offset = dtohl(entry);
+            res_idx = (short) entry_idx;
+          }
+
           if (offset != ResTable_type.NO_ENTRY) {
-            // const ResTable_entry* entry =
-            //     reinterpret_cast<const ResTable_entry*>(reinterpret_cast<const uint8_t*>(type.type) +
-            //     dtohl(type.type.entriesStart) + offset);
+            // auto entry = type.offset(dtohl(type->entriesStart) +
+            // offset).convert<ResTable_entry>();
             ResTable_entry entry =
-                new ResTable_entry(type.myBuf(), type.myOffset() +
-                    dtohl(type.entriesStart) + offset);
+                new ResTable_entry(
+                    type.myBuf(), type.myOffset() + dtohl(type.entriesStart) + offset);
+            // if (!entry) {
+            //   return base::unexpected(IOError::PAGES_MISSING);
+            // }
+            // TODO implement above
+            // if (entry->key() == static_cast<uint32_t>(*key_idx)) {
             if (dtohl(entry.getKeyIndex()) == key_idx) {
-              // The package ID will be overridden by the caller (due to runtime assignment of package
+              // The package ID will be overridden by the caller (due to runtime assignment of
+              // package
               // IDs for shared libraries).
-              return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), (short) entry_idx);
+              // return make_resid(0x00, *type_idx + type_id_offset_ + 1, res_idx);
+              return make_resid((byte) 0x00, (byte) (type_idx + type_id_offset_ + 1), res_idx);
             }
           }
         }
       }
+      // return base::unexpected(std::nullopt);
       return 0;
     }
 
diff --git a/robolectric/Android.bp b/robolectric/Android.bp
index 139062c..e690895 100644
--- a/robolectric/Android.bp
+++ b/robolectric/Android.bp
@@ -15,6 +15,7 @@
     name: "Robolectric_robolectric_upstream",
     libs: [
         "Robolectric_shadows_framework_upstream",
+        "Robolectric_shadows_versioning_upstream",
         "Robolectric_annotations_upstream",
         "Robolectric_shadowapi_upstream",
         "Robolectric_resources_upstream",
diff --git a/robolectric/build.gradle b/robolectric/build.gradle
index faaa8b3..b826e92 100644
--- a/robolectric/build.gradle
+++ b/robolectric/build.gradle
@@ -5,8 +5,8 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
-    annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+    annotationProcessor libs.auto.service
+    annotationProcessor libs.error.prone.core
 
     api project(":annotations")
     api project(":junit")
@@ -16,33 +16,34 @@
     api project(":utils")
     api project(":utils:reflector")
     api project(":plugins:maven-dependency-resolver")
-    api "javax.inject:javax.inject:1"
-    compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
-    api "javax.annotation:javax.annotation-api:1.3.2"
+    api libs.javax.inject
+    compileOnly libs.auto.service.annotations
+    api libs.javax.annotation.api
 
     // We need to have shadows-framework.jar on the runtime system classpath so ServiceLoader
     //   can find its META-INF/services/org.robolectric.shadows.ShadowAdapter.
     api project(":shadows:framework")
 
-    implementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2'
-    api "org.bouncycastle:bcprov-jdk18on:1.72"
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    implementation libs.conscrypt.openjdk.uber
+    api libs.bcprov.jdk18on
+    compileOnly libs.findbugs.jsr305
 
     compileOnly AndroidSdk.MAX_SDK.coordinates
-    compileOnly "junit:junit:${junitVersion}"
+    compileOnly libs.junit4
+
     api "androidx.test:monitor:$axtMonitorVersion@aar"
     implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion@aar"
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "com.google.truth.extensions:truth-java8-extension:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-    testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.truth.java8.extension
+    testImplementation libs.mockito
+    testImplementation libs.hamcrest.junit
     testImplementation "androidx.test:core:$axtCoreVersion@aar"
     testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
     testImplementation "androidx.test.ext:truth:$axtTruthVersion@aar"
     testImplementation "androidx.test:runner:$axtRunnerVersion@aar"
-    testImplementation("com.google.guava:guava:$guavaJREVersion")
+    testImplementation libs.guava
     testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports
 }
diff --git a/robolectric/src/main/java/org/robolectric/Robolectric.java b/robolectric/src/main/java/org/robolectric/Robolectric.java
index 47a52c5..3ce637e 100644
--- a/robolectric/src/main/java/org/robolectric/Robolectric.java
+++ b/robolectric/src/main/java/org/robolectric/Robolectric.java
@@ -1,5 +1,6 @@
 package org.robolectric;
 
+import static com.google.common.base.Preconditions.checkState;
 import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
 
 import android.annotation.IdRes;
@@ -11,6 +12,7 @@
 import android.content.ContentProvider;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Looper;
 import android.util.AttributeSet;
 import android.view.View;
 import javax.annotation.Nullable;
@@ -104,6 +106,9 @@
    */
   public static <T extends Activity> ActivityController<T> buildActivity(
       Class<T> activityClass, Intent intent, @Nullable Bundle activityOptions) {
+    checkState(
+        Thread.currentThread() == Looper.getMainLooper().getThread(),
+        "buildActivity must be called on main Looper thread");
     return ActivityController.of(
         ReflectionHelpers.callConstructor(activityClass), intent, activityOptions);
   }
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
old mode 100755
new mode 100644
index a807d4e..ed859dd
--- a/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java
@@ -361,7 +361,7 @@
         // Preload fonts resources
         FontsContract.setApplicationContextForResources(application);
       }
-      registerBroadcastReceivers(application, appManifest);
+      registerBroadcastReceivers(application, appManifest, loadedApk);
 
       appResources.updateConfiguration(androidConfiguration, displayMetrics);
       // propagate any updates to configuration via RuntimeEnvironment.setQualifiers
@@ -412,6 +412,11 @@
       Path packageFile = appManifest.getApkFile();
       parsedPackage = ShadowPackageParser.callParsePackage(packageFile);
     }
+    if (parsedPackage != null
+        && parsedPackage.applicationInfo != null
+        && RuntimeEnvironment.getApiLevel() >= P) {
+      parsedPackage.applicationInfo.appComponentFactory = appManifest.getAppComponentFactory();
+    }
     return parsedPackage;
   }
 
@@ -691,15 +696,39 @@
         .toString();
   }
 
+  private static BroadcastReceiver newBroadcastReceiverFromP(
+      String receiverClassName, LoadedApk loadedApk) {
+    ClassLoader classLoader = Shadow.class.getClassLoader();
+    if (loadedApk == null || loadedApk.getAppFactory() == null) {
+      return (BroadcastReceiver) newInstanceOf(receiverClassName);
+    } else {
+      try {
+        return loadedApk.getAppFactory().instantiateReceiver(classLoader, receiverClassName, null);
+      } catch (ReflectiveOperationException e) {
+        Logger.warn(
+            "Failed to initialize receiver %s with AppComponentFactory %s: %s",
+            receiverClassName, loadedApk.getAppFactory(), e);
+      }
+    }
+    return null;
+  }
+
   // TODO move/replace this with packageManager
   @VisibleForTesting
-  static void registerBroadcastReceivers(Application application, AndroidManifest androidManifest) {
+  static void registerBroadcastReceivers(
+      Application application, AndroidManifest androidManifest, LoadedApk loadedApk) {
     for (BroadcastReceiverData receiver : androidManifest.getBroadcastReceivers()) {
       IntentFilter filter = new IntentFilter();
       for (String action : receiver.getActions()) {
         filter.addAction(action);
       }
-      application.registerReceiver((BroadcastReceiver) newInstanceOf(receiver.getName()), filter);
+      String receiverClassName = receiver.getName();
+      if (loadedApk != null && RuntimeEnvironment.getApiLevel() >= P) {
+        application.registerReceiver(
+            newBroadcastReceiverFromP(receiverClassName, loadedApk), filter);
+      } else {
+        application.registerReceiver((BroadcastReceiver) newInstanceOf(receiverClassName), filter);
+      }
     }
   }
 
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index a6908af..dd1bc5c 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -157,7 +157,7 @@
 
   @Override
   public void loopMainThreadUntilIdle() {
-    if (!ShadowLooper.looperMode().equals(LooperMode.Mode.PAUSED)) {
+    if (ShadowLooper.looperMode().equals(LooperMode.Mode.LEGACY)) {
       shadowMainLooper().idle();
     } else {
       ImmutableSet<IdlingResourceProxy> idlingResources = syncIdlingResources();
diff --git a/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java b/robolectric/src/main/java/org/robolectric/internal/AndroidSandbox.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java b/robolectric/src/main/java/org/robolectric/internal/dependency/PropertiesDependencyResolver.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
index c8b2693..cf3af7d 100644
--- a/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
+++ b/robolectric/src/main/java/org/robolectric/plugins/DefaultSdkProvider.java
@@ -1,5 +1,23 @@
 package org.robolectric.plugins;
 
+import com.google.auto.service.AutoService;
+import com.google.common.base.Preconditions;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import org.robolectric.internal.dependency.DependencyJar;
+import org.robolectric.internal.dependency.DependencyResolver;
+import org.robolectric.pluginapi.Sdk;
+import org.robolectric.pluginapi.SdkProvider;
+import org.robolectric.shadows.ShadowBuild;
+import org.robolectric.util.Util;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
@@ -17,28 +35,9 @@
 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 static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 
 import android.os.Build;
 
-import com.google.auto.service.AutoService;
-import com.google.common.base.Preconditions;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Locale;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import javax.annotation.Priority;
-import javax.inject.Inject;
-import org.robolectric.internal.dependency.DependencyJar;
-import org.robolectric.internal.dependency.DependencyResolver;
-import org.robolectric.pluginapi.Sdk;
-import org.robolectric.pluginapi.SdkProvider;
-import org.robolectric.util.Util;
-
 /**
  * Robolectric's default {@link SdkProvider}.
  *
@@ -85,8 +84,6 @@
     knownSdks.put(S, new DefaultSdk(S, "12", "7732740", "REL", 9));
     knownSdks.put(S_V2, new DefaultSdk(S_V2, "12.1", "8229987", "REL", 9));
     knownSdks.put(TIRAMISU, new DefaultSdk(TIRAMISU, "13", "9030017", "Tiramisu", 9));
-    // TODO(rexhoffman): should this have a dedicated mechanism?  Should we maintain a known good version?
-    knownSdks.put(CUR_DEVELOPMENT, new DefaultSdk(CUR_DEVELOPMENT, "current", "r0", "UpsideDownCake", 9));
   }
 
   @Override
diff --git a/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java
new file mode 100644
index 0000000..22df750
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/CustomAppComponentFactory.java
@@ -0,0 +1,22 @@
+package org.robolectric;
+
+import android.app.AppComponentFactory;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;
+
+public final class CustomAppComponentFactory extends AppComponentFactory {
+  @Override
+  public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
+      throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+    if (className != null) {
+      if (className.contains(CustomConstructorWithOneActionReceiver.class.getName())) {
+        return new CustomConstructorWithOneActionReceiver(100);
+      } else if (className.contains(CustomConstructorWithEmptyActionReceiver.class.getName())) {
+        return new CustomConstructorWithEmptyActionReceiver(100);
+      }
+    }
+    return super.instantiateReceiver(cl, className, intent);
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java
new file mode 100644
index 0000000..1132f6e
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/CustomConstructorReceiverWrapper.java
@@ -0,0 +1,32 @@
+package org.robolectric;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class CustomConstructorReceiverWrapper {
+  private static class CustomConstructorReceiver extends BroadcastReceiver {
+    private final int intValue;
+
+    public CustomConstructorReceiver(int intValue) {
+      // We don't use intValue actually, and we only want to use this class to test the
+      // initialization of BroadcastReceiver with a custom constructor.
+      this.intValue = intValue;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {}
+  }
+
+  public static class CustomConstructorWithOneActionReceiver extends CustomConstructorReceiver {
+    public CustomConstructorWithOneActionReceiver(int intValue) {
+      super(intValue);
+    }
+  }
+
+  public static class CustomConstructorWithEmptyActionReceiver extends CustomConstructorReceiver {
+    public CustomConstructorWithEmptyActionReceiver(int intValue) {
+      super(intValue);
+    }
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
index 0ba55d8..c0108de 100644
--- a/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
+++ b/robolectric/src/test/java/org/robolectric/RobolectricTestRunnerTest.java
@@ -50,6 +50,7 @@
 import org.robolectric.annotation.Config.Implementation;
 import org.robolectric.annotation.experimental.LazyApplication;
 import org.robolectric.annotation.experimental.LazyApplication.LazyLoad;
+import org.robolectric.config.ConfigurationRegistry;
 import org.robolectric.internal.AndroidSandbox.TestEnvironmentSpec;
 import org.robolectric.internal.ResourcesMode;
 import org.robolectric.internal.ShadowProvider;
@@ -163,10 +164,10 @@
     assertThat(events)
         .containsExactly(
             "started: first",
-            "failure: ShadowActivityThread.reset: ActivityThread not set",
+            "failure: fake error in setUpApplicationState",
             "finished: first",
             "started: second",
-            "failure: ShadowActivityThread.reset: ActivityThread not set",
+            "failure: fake error in setUpApplicationState",
             "finished: second")
         .inOrder();
   }
@@ -319,6 +320,9 @@
     @Override
     public void setUpApplicationState(Method method,
         Configuration configuration, AndroidManifest appManifest) {
+      // ConfigurationRegistry.instance is required for resetters.
+      Config config = configuration.get(Config.class);
+      ConfigurationRegistry.instance = new ConfigurationRegistry(configuration.map());
       throw new RuntimeException("fake error in setUpApplicationState");
     }
   }
diff --git a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
index 87ddb07..0428c44 100644
--- a/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/DrawableResourceLoaderTest.java
@@ -3,9 +3,9 @@
 import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assume.assumeTrue;
 import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
 
 import android.animation.Animator;
@@ -31,7 +31,7 @@
 
   @Before
   public void setup() throws Exception {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
     resources = ApplicationProvider.getApplicationContext().getResources();
   }
 
diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
index b895d65..15ca52c 100644
--- a/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/ResourceLoaderTest.java
@@ -2,7 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.O;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
 
 import android.content.res.Configuration;
@@ -32,7 +32,7 @@
 
   @Before
   public void setUp() {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
 
     optsForO = RuntimeEnvironment.getApiLevel() >= O
         ? "nowidecg-lowdr-"
@@ -71,7 +71,11 @@
 
   private void checkForPollutionHelper() {
     assertThat(RuntimeEnvironment.getQualifiers())
-        .isEqualTo("en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-" + optsForO + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v" + Build.VERSION.RESOURCES_SDK_INT);
+        .isEqualTo(
+            "en-rUS-ldltr-sw320dp-w320dp-h470dp-normal-notlong-notround-"
+                + optsForO
+                + "port-notnight-mdpi-finger-keyssoft-nokeys-navhidden-nonav-v"
+                + Build.VERSION.RESOURCES_SDK_INT);
 
     View view =
         LayoutInflater.from(ApplicationProvider.getApplicationContext())
@@ -97,7 +101,10 @@
     assertThat(resId).isNotNull();
     assertThat(resourceProvider.getResName(resId)).isEqualTo(internalResource);
 
-    Class<?> internalRIdClass = Robolectric.class.getClassLoader().loadClass("com.android.internal.R$" + internalResource.type);
+    Class<?> internalRIdClass =
+        Robolectric.class
+            .getClassLoader()
+            .loadClass("com.android.internal.R$" + internalResource.type);
     int internalResourceId;
     internalResourceId = (Integer) internalRIdClass.getDeclaredField(internalResource.name).get(null);
     assertThat(resId).isEqualTo(internalResourceId);
diff --git a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
index 0ae4675..d4395c5 100644
--- a/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/ResourceTableFactoryIntegrationTest.java
@@ -1,7 +1,7 @@
 package org.robolectric.android;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
 
 import android.os.Build;
@@ -17,7 +17,7 @@
 public class ResourceTableFactoryIntegrationTest {
   @Test
   public void shouldIncludeStyleableAttributesThatDoNotHaveACorrespondingEntryInAttrClass() throws Exception {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
     // This covers a corner case in Framework resources where an attribute is mentioned in a styleable array, e.g: R.styleable.Toolbar_buttonGravity but there is no corresponding R.attr.buttonGravity
     assertThat(RuntimeEnvironment.getSystemResourceTable()
           .getResourceId(new ResName("android", "attr", "buttonGravity"))).isGreaterThan(0);
diff --git a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
index b57f606..0c8d977 100644
--- a/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/XmlResourceParserImplTest.java
@@ -2,11 +2,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.asList;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 
 import android.app.Application;
 import android.content.res.XmlResourceParser;
@@ -276,7 +276,7 @@
 
   @Test
   public void testIsWhitespace() throws Exception {
-    assumeTrue(RuntimeEnvironment.useLegacyResources());
+    assume().that(RuntimeEnvironment.useLegacyResources()).isTrue();
 
     XmlResourceParserImpl parserImpl = (XmlResourceParserImpl) parser;
     assertThat(parserImpl.isWhitespace("bar")).isFalse();
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
index 6edc428..dc6ac31 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentCreateApplicationTest.java
@@ -88,7 +88,7 @@
     Application application = AndroidTestEnvironment.createApplication(appManifest, null,
         new ApplicationInfo());
     shadowOf(application).callAttach(RuntimeEnvironment.systemContext);
-    registerBroadcastReceivers(application, appManifest);
+    registerBroadcastReceivers(application, appManifest, null);
 
     List<ShadowApplication.Wrapper> receivers = shadowOf(application).getRegisteredReceivers();
     assertThat(receivers).hasSize(1);
diff --git a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
index 0511881..70dd385 100644
--- a/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
+++ b/robolectric/src/test/java/org/robolectric/android/internal/AndroidTestEnvironmentTest.java
@@ -2,8 +2,8 @@
 
 import static android.os.Build.VERSION_CODES.O;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.robolectric.annotation.ConscryptMode.Mode.OFF;
 import static org.robolectric.annotation.ConscryptMode.Mode.ON;
 import static org.robolectric.annotation.LooperMode.Mode.LEGACY;
@@ -241,7 +241,7 @@
   @Test
   public void testResourceNotFound() {
     // not relevant for binary resources mode
-    assumeTrue(bootstrapWrapper.isLegacyResources());
+    assume().that(bootstrapWrapper.isLegacyResources()).isTrue();
 
     try {
       bootstrapWrapper.changeAppManifest(new ThrowingManifest(bootstrapWrapper.getAppManifest()));
diff --git a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
index e3163cc..af1d8ba 100644
--- a/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
+++ b/robolectric/src/test/java/org/robolectric/interceptors/AndroidInterceptorsIntegrationTest.java
@@ -66,10 +66,10 @@
 
   @Test
   public void systemNanoTime_shouldReturnShadowClockTime() throws Throwable {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      SystemClock.setCurrentTimeMillis(200);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+    } else {
+      SystemClock.setCurrentTimeMillis(200);
     }
 
     long nanoTime = invokeDynamic(System.class, "nanoTime", long.class);
@@ -78,10 +78,10 @@
 
   @Test
   public void systemCurrentTimeMillis_shouldReturnShadowClockTime() throws Throwable {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      SystemClock.setCurrentTimeMillis(200);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowSystemClock.setNanoTime(Duration.ofMillis(200).toNanos());
+    } else {
+      SystemClock.setCurrentTimeMillis(200);
     }
 
     long currentTimeMillis = invokeDynamic(System.class, "currentTimeMillis", long.class);
@@ -187,6 +187,6 @@
         callsite
             .dynamicInvoker()
             .invokeWithArguments(
-                Arrays.stream(params).map(param -> param.val).collect(Collectors.toList()));
+                Arrays.stream(params).map(param -> param.value).collect(Collectors.toList()));
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java b/robolectric/src/test/java/org/robolectric/internal/MavenManifestFactoryTest.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java b/robolectric/src/test/java/org/robolectric/internal/dependency/PropertiesDependencyResolverTest.java
old mode 100755
new mode 100644
diff --git a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
index 8b493ba..dc20266 100644
--- a/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/res/StyleResourceLoaderTest.java
@@ -2,7 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.JELLY_BEAN;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.robolectric.util.TestUtil.sdkResources;
 
 import org.junit.Before;
@@ -18,14 +18,16 @@
 
   @Before
   public void setUp() throws Exception {
-    assumeTrue(RuntimeEnvironment.useLegacyResources());
+    assume().that(RuntimeEnvironment.useLegacyResources()).isTrue();
     ResourcePath resourcePath = sdkResources(JELLY_BEAN);
     resourceTable = new ResourceTableFactory().newResourceTable("android", resourcePath);
   }
 
   @Test
   public void testStyleDataIsLoadedCorrectly() throws Exception {
-    TypedResource typedResource = resourceTable.getValue(new ResName("android", "style", "Theme_Holo"), new ResTable_config());
+    TypedResource typedResource =
+        resourceTable.getValue(
+            new ResName("android", "style", "Theme_Holo"), new ResTable_config());
     StyleData styleData = (StyleData) typedResource.getData();
     assertThat(styleData.getName()).isEqualTo("Theme_Holo");
     assertThat(styleData.getParent()).isEqualTo("Theme");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
new file mode 100644
index 0000000..72887dd
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityLteBuilderTest.java
@@ -0,0 +1,114 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellIdentityLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilderTest {
+
+  private static final String MCC = "310";
+  private static final String MNC = "260";
+  private static final int CI = 0;
+  private static final int PCI = 1;
+  private static final int TAC = 2;
+  private static final int EARFCN = 4;
+  private static final int[] BANDS = new int[] {2, 4};
+  private static final int BANDWIDTH = 5;
+  private static final String SHORT_OPERATOR_NAME = "short operator name";
+  private static final String LONG_OPERATOR_NAME = "long operator name";
+  private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240");
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellIdentityLte cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+
+    assertThat(cellIdentity.getCi()).isEqualTo(CellInfo.UNAVAILABLE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.M)
+  public void build_sdkJtoM() {
+    CellIdentityLte cellIdentity = getCellIdentityLte();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.N, maxSdk = Build.VERSION_CODES.O_MR1)
+  public void build_sdkNtoO() {
+    CellIdentityLte cellIdentity = getCellIdentityLte();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+    assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+  public void build_sdkPtoQ() {
+    CellIdentityLte cellIdentity = getCellIdentityLte();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+    assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+    assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+    assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+    assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+    assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+    assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.S)
+  public void build_fromSdkS() {
+    CellIdentityLte cellIdentity = getCellIdentityLte();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+    assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+    assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+    assertThat(cellIdentity.getEarfcn()).isEqualTo(EARFCN);
+    assertThat(cellIdentity.getBandwidth()).isEqualTo(BANDWIDTH);
+    assertThat(cellIdentity.getBands()).isEqualTo(BANDS);
+    assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+    assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+    assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS);
+  }
+
+  /**
+   * Assertions on {@link android.telephony.CellIdentityLte} values that are common across all
+   * tested SDKs.
+   */
+  private void assertCellIdentityFieldsForAllSdks(CellIdentityLte cellIdentity) {
+    assertThat(cellIdentity.getMcc()).isEqualTo(Integer.parseInt(MCC));
+    assertThat(cellIdentity.getMnc()).isEqualTo(Integer.parseInt(MNC));
+    assertThat(cellIdentity.getCi()).isEqualTo(CI);
+    assertThat(cellIdentity.getPci()).isEqualTo(PCI);
+    assertThat(cellIdentity.getTac()).isEqualTo(TAC);
+  }
+
+  private CellIdentityLte getCellIdentityLte() {
+    return CellIdentityLteBuilder.newBuilder()
+        .setMcc(MCC)
+        .setMnc(MNC)
+        .setCi(CI)
+        .setPci(PCI)
+        .setTac(TAC)
+        .setEarfcn(EARFCN)
+        .setBands(BANDS)
+        .setBandwidth(BANDWIDTH)
+        .setLongOperatorName(LONG_OPERATOR_NAME)
+        .setShortOperatorName(SHORT_OPERATOR_NAME)
+        .setAdditionalPlmns(ADDITIONAL_PLMNS)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java
new file mode 100644
index 0000000..8474b21
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellIdentityNrBuilderTest.java
@@ -0,0 +1,90 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellIdentityNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellIdentityNrBuilderTest {
+
+  private static final int PCI = 1;
+  private static final int TAC = 2;
+  private static final int NRARFCN = 4;
+  private static final int[] BANDS =
+      new int[] {
+        AccessNetworkConstants.NgranBands.BAND_1, AccessNetworkConstants.NgranBands.BAND_2
+      };
+  private static final String MCC = "310";
+  private static final String MNC = "260";
+  private static final int NCI = 0;
+  private static final String LONG_OPERATOR_NAME = "long operator name";
+  private static final String SHORT_OPERATOR_NAME = "short operator name";
+  private static final ImmutableList<String> ADDITIONAL_PLMNS = ImmutableList.of("310240");
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellIdentityNr cellIdentity = CellIdentityNrBuilder.newBuilder().build();
+
+    assertThat(cellIdentity.getPci()).isEqualTo(CellInfo.UNAVAILABLE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+  public void build_sdkQtoR() {
+    CellIdentityNr cellIdentity = getCellIdentityNr();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.S)
+  public void build_fromSdkS() {
+    CellIdentityNr cellIdentity = getCellIdentityNr();
+
+    assertCellIdentityFieldsForAllSdks(cellIdentity);
+    assertThat(cellIdentity.getBands()).isEqualTo(BANDS);
+    assertThat(cellIdentity.getAdditionalPlmns()).containsExactlyElementsIn(ADDITIONAL_PLMNS);
+  }
+
+  /**
+   * Assertions on {@link android.telephony.CellIdentityNr} values that are common across all tested
+   * SDKs.
+   */
+  private void assertCellIdentityFieldsForAllSdks(CellIdentityNr cellIdentity) {
+    assertThat(cellIdentity.getPci()).isEqualTo(PCI);
+    assertThat(cellIdentity.getTac()).isEqualTo(TAC);
+    assertThat(cellIdentity.getNrarfcn()).isEqualTo(NRARFCN);
+    assertThat(cellIdentity.getMccString()).isEqualTo(MCC);
+    assertThat(cellIdentity.getMncString()).isEqualTo(MNC);
+    assertThat(cellIdentity.getNci()).isEqualTo(NCI);
+    assertThat(cellIdentity.getOperatorAlphaLong().toString()).isEqualTo(LONG_OPERATOR_NAME);
+    assertThat(cellIdentity.getOperatorAlphaShort().toString()).isEqualTo(SHORT_OPERATOR_NAME);
+  }
+
+  private CellIdentityNr getCellIdentityNr() {
+    return CellIdentityNrBuilder.newBuilder()
+        .setPci(PCI)
+        .setTac(TAC)
+        .setNrarfcn(NRARFCN)
+        .setBands(BANDS)
+        .setMcc(MCC)
+        .setMnc(MNC)
+        .setNci(NCI)
+        .setLongOperatorName(LONG_OPERATOR_NAME)
+        .setShortOperatorName(SHORT_OPERATOR_NAME)
+        .setAdditionalPlmns(ADDITIONAL_PLMNS)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
new file mode 100644
index 0000000..f61abad
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoLteBuilderTest.java
@@ -0,0 +1,81 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellInfoLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilderTest {
+
+  private static final boolean REGISTERED = false;
+  private static final long TIMESTAMP_NANOS = 123L;
+  private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis();
+  private static final int CELL_CONNECTION_STATUS = 1;
+
+  private static final CellIdentityLte cellIdentity =
+      CellIdentityLteBuilder.newBuilder().setMcc("310").build();
+  private static final CellSignalStrengthLte cellSignalStrength =
+      CellSignalStrengthLteBuilder.newBuilder().setRsrp(-120).build();
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellInfoLte cellInfo = CellInfoLteBuilder.newBuilder().build();
+
+    assertThat(cellInfo.getTimeStamp()).isEqualTo(0);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+  public void build_sdkJtoN() {
+    CellInfoLte cellInfo = getCellInfoLte();
+
+    assertThat(cellInfo.isRegistered()).isFalse();
+    assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+    assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.P, maxSdk = Build.VERSION_CODES.Q)
+  public void build_fromSdkPtoQ() {
+    CellInfoLte cellInfo = getCellInfoLte();
+
+    assertThat(cellInfo.isRegistered()).isFalse();
+    assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+    assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+    assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.R, maxSdk = Config.NEWEST_SDK)
+  public void build_fromSdkR() {
+    CellInfoLte cellInfo = getCellInfoLte();
+
+    assertThat(cellInfo.isRegistered()).isFalse();
+    assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS);
+    assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+    assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+    assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity);
+  }
+
+  private CellInfoLte getCellInfoLte() {
+    return CellInfoLteBuilder.newBuilder()
+        .setRegistered(REGISTERED)
+        .setTimeStampNanos(TIMESTAMP_NANOS)
+        .setCellConnectionStatus(CELL_CONNECTION_STATUS)
+        .setCellIdentity(cellIdentity)
+        .setCellSignalStrength(cellSignalStrength)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java
new file mode 100644
index 0000000..80e7dcc
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellInfoNrBuilderTest.java
@@ -0,0 +1,76 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfoNr;
+import android.telephony.CellSignalStrengthNr;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.time.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellInfoNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellInfoNrBuilderTest {
+
+  private static final boolean REGISTERED = false;
+  private static final long TIMESTAMP_NANOS = 123L;
+  private static final long TIMESTAMP_MILLIS = Duration.ofNanos(TIMESTAMP_NANOS).toMillis();
+  private static final int CELL_CONNECTION_STATUS = 1;
+
+  private static final CellIdentityNr cellIdentity =
+      CellIdentityNrBuilder.newBuilder().setMcc("310").build();
+  private static final CellSignalStrengthNr cellSignalStrength =
+      CellSignalStrengthNrBuilder.newBuilder().setCsiRsrp(-100).build();
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellInfoNr cellInfo = CellInfoNrBuilder.newBuilder().build();
+
+    assertThat(cellInfo.getTimeStamp()).isEqualTo(0);
+  }
+
+  @Test
+  @Config(sdk = Build.VERSION_CODES.Q)
+  public void build_sdkQ() {
+    CellInfoNr cellInfo = getCellInfoNr();
+
+    assertCellInfoFieldsForAllSdks(cellInfo);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.R)
+  public void build_fromSdkR() {
+    CellInfoNr cellInfo = getCellInfoNr();
+
+    assertCellInfoFieldsForAllSdks(cellInfo);
+    assertThat(cellInfo.getTimestampMillis()).isEqualTo(TIMESTAMP_MILLIS);
+  }
+
+  /**
+   * Assertions on {@link android.telephony.CellInfo} values that are common across all tested SDKs.
+   */
+  private void assertCellInfoFieldsForAllSdks(CellInfoNr cellInfo) {
+    assertThat(cellInfo.isRegistered()).isFalse();
+    assertThat(cellInfo.getTimeStamp()).isEqualTo(TIMESTAMP_NANOS);
+    assertThat(cellInfo.getCellConnectionStatus()).isEqualTo(CELL_CONNECTION_STATUS);
+    assertThat(cellInfo.getCellIdentity()).isEqualTo(cellIdentity);
+    assertThat(cellInfo.getCellSignalStrength()).isEqualTo(cellSignalStrength);
+  }
+
+  private CellInfoNr getCellInfoNr() {
+    return CellInfoNrBuilder.newBuilder()
+        .setRegistered(REGISTERED)
+        .setTimeStampNanos(TIMESTAMP_NANOS)
+        .setCellConnectionStatus(CELL_CONNECTION_STATUS)
+        .setCellIdentity(cellIdentity)
+        .setCellSignalStrength(cellSignalStrength)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
new file mode 100644
index 0000000..cfd3bbe
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthLteBuilderTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellSignalStrengthLteBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilderTest {
+
+  // The platform enforces that some of these values are within a certain range - otherwise, it will
+  // default to {@link android.telephony.CellInfo.UNAVAILABLE}.
+  private static final int RSSI = -100;
+  private static final int RSRP = -120;
+  private static final int RSRQ = -10;
+  private static final int RSSNR = 30;
+  private static final int CQI_TABLE_INDEX = 4;
+  private static final int CQI = 5;
+  private static final int TIMING_ADVANCE = 6;
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellSignalStrengthLte cellSignalStrength = CellSignalStrengthLteBuilder.newBuilder().build();
+
+    assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(CellInfo.UNAVAILABLE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.JELLY_BEAN_MR1, maxSdk = Build.VERSION_CODES.N_MR1)
+  public void build_sdkJtoN() {
+    CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+    assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.O, maxSdk = Build.VERSION_CODES.P)
+  public void build_sdkOToP() {
+    CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+    assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+    assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+    assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+    assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+    assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.R)
+  public void build_sdkQtoR() {
+    CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+    assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+    assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+    assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+    assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+    assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+    assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.S)
+  public void build_fromSdkS() {
+    CellSignalStrengthLte cellSignalStrength = getCellSignalStrength();
+
+    assertThat(cellSignalStrength.getRssi()).isEqualTo(RSSI);
+    assertThat(cellSignalStrength.getRsrp()).isEqualTo(RSRP);
+    assertThat(cellSignalStrength.getRsrq()).isEqualTo(RSRQ);
+    assertThat(cellSignalStrength.getRssnr()).isEqualTo(RSSNR);
+    assertThat(cellSignalStrength.getCqiTableIndex()).isEqualTo(CQI_TABLE_INDEX);
+    assertThat(cellSignalStrength.getCqi()).isEqualTo(CQI);
+    assertThat(cellSignalStrength.getTimingAdvance()).isEqualTo(TIMING_ADVANCE);
+  }
+
+  private CellSignalStrengthLte getCellSignalStrength() {
+    return CellSignalStrengthLteBuilder.newBuilder()
+        .setRssi(RSSI)
+        .setRsrp(RSRP)
+        .setRsrq(RSRQ)
+        .setRssnr(RSSNR)
+        .setCqi(CQI)
+        .setCqiTableIndex(CQI_TABLE_INDEX)
+        .setTimingAdvance(TIMING_ADVANCE)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java
new file mode 100644
index 0000000..1f18ee8
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/CellSignalStrengthNrBuilderTest.java
@@ -0,0 +1,84 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthNr;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link CellSignalStrengthNrBuilder} */
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = Build.VERSION_CODES.Q)
+public class CellSignalStrengthNrBuilderTest {
+
+  // The platform enforces that some of these values are within a certain range - otherwise, it will
+  // default to {@link android.telephony.CellInfo.UNAVAILABLE}.
+  private static final int CSI_RSRP = -100;
+  private static final int CSI_RSRQ = -10;
+  private static final int CSI_SINR = -20;
+  private static final int CSI_CQI_TABLE_INDEX = 1;
+  private static final ImmutableList<Byte> CSI_CQI_REPORT = ImmutableList.of((byte) 7);
+  private static final int SS_RSRP = -140;
+  private static final int SS_RSRQ = -15;
+  private static final int SS_SINR = -20;
+  private static final int TIMING_ADVANCE = 10;
+
+  @Test
+  public void build_noArguments() {
+    // The intent is to primarily verify that there are no issues setting default values i.e., no
+    // exceptions thrown or invalid inputs.
+    CellSignalStrengthNr cellSignalStrength = CellSignalStrengthNrBuilder.newBuilder().build();
+
+    assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CellInfo.UNAVAILABLE);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.Q, maxSdk = Build.VERSION_CODES.S_V2)
+  public void build_sdkQtoS() {
+    CellSignalStrengthNr cellSignalStrength = getCellSignalStrength();
+
+    assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength);
+  }
+
+  @Test
+  @Config(minSdk = Build.VERSION_CODES.TIRAMISU)
+  public void build_fromSdkT() {
+    CellSignalStrengthNr cellSignalStrength = getCellSignalStrength();
+
+    assertCellSignalStrengthFieldsForAllSdks(cellSignalStrength);
+    assertThat(cellSignalStrength.getCsiCqiTableIndex()).isEqualTo(CSI_CQI_TABLE_INDEX);
+    assertThat(cellSignalStrength.getCsiCqiReport()).containsExactly(7);
+  }
+
+  /**
+   * Assertions on {@link android.telephony.CellSignalStrengthNr} values that are common across all
+   * tested SDKs.
+   */
+  private void assertCellSignalStrengthFieldsForAllSdks(CellSignalStrengthNr cellSignalStrength) {
+    assertThat(cellSignalStrength.getCsiRsrp()).isEqualTo(CSI_RSRP);
+    assertThat(cellSignalStrength.getCsiRsrq()).isEqualTo(CSI_RSRQ);
+    assertThat(cellSignalStrength.getCsiSinr()).isEqualTo(CSI_SINR);
+    assertThat(cellSignalStrength.getSsRsrp()).isEqualTo(SS_RSRP);
+    assertThat(cellSignalStrength.getSsRsrq()).isEqualTo(SS_RSRQ);
+    assertThat(cellSignalStrength.getSsSinr()).isEqualTo(SS_SINR);
+  }
+
+  private CellSignalStrengthNr getCellSignalStrength() {
+    return CellSignalStrengthNrBuilder.newBuilder()
+        .setCsiRsrp(CSI_RSRP)
+        .setCsiRsrq(CSI_RSRQ)
+        .setCsiSinr(CSI_SINR)
+        .setCsiCqiTableIndex(CSI_CQI_TABLE_INDEX)
+        .setCsiCqiReport(CSI_CQI_REPORT)
+        .setSsRsrp(SS_RSRP)
+        .setSsRsrq(SS_RSRQ)
+        .setSsSinr(SS_SINR)
+        .setTimingAdvance(TIMING_ADVANCE)
+        .build();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
index 27e635c..06769c6 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/MediaCodecInfoBuilderTest.java
@@ -14,6 +14,7 @@
 import android.media.MediaCodecInfo.CodecProfileLevel;
 import android.media.MediaCodecList;
 import android.media.MediaFormat;
+import android.util.Range;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,6 +30,10 @@
   private static final String VP9_DECODER_NAME = "test.decoder.vp9";
   private static final String MULTIFORMAT_ENCODER_NAME = "test.encoder.multiformat";
 
+  private static final int WIDTH = 1920;
+  private static final int HEIGHT = 1080;
+  private static final Range<Integer> DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE = new Range<>(2, 896);
+
   private static final MediaFormat AAC_MEDIA_FORMAT =
       createMediaFormat(
           MIMETYPE_AUDIO_AAC, new String[] {CodecCapabilities.FEATURE_DynamicTimestamp});
@@ -37,6 +42,9 @@
           MIMETYPE_AUDIO_OPUS, new String[] {CodecCapabilities.FEATURE_AdaptivePlayback});
   private static final MediaFormat AVC_MEDIA_FORMAT =
       createMediaFormat(MIMETYPE_VIDEO_AVC, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
+  private static final MediaFormat AVC_MEDIA_FORMAT_WITH_RESOLUTION =
+      createMediaFormat(
+          MIMETYPE_VIDEO_AVC, WIDTH, HEIGHT, new String[] {CodecCapabilities.FEATURE_IntraRefresh});
   private static final MediaFormat VP9_MEDIA_FORMAT =
       createMediaFormat(
           MIMETYPE_VIDEO_VP9,
@@ -123,6 +131,10 @@
     assertThat(codecCapabilities.getMimeType()).isEqualTo(MIMETYPE_VIDEO_AVC);
     assertThat(codecCapabilities.getAudioCapabilities()).isNull();
     assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+    assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+        .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
+    assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+        .isEqualTo(DEFAULT_SUPPORTED_VIDEO_SIZE_RANGE);
     assertThat(codecCapabilities.getEncoderCapabilities()).isNotNull();
     assertThat(codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_IntraRefresh))
         .isTrue();
@@ -136,6 +148,24 @@
 
   @Test
   @Config(minSdk = Q)
+  public void canCreateVideoEncoderCapabilities_supportedFormatResolutionIsSet() {
+    CodecCapabilities codecCapabilities =
+        MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
+            .setMediaFormat(AVC_MEDIA_FORMAT_WITH_RESOLUTION)
+            .setIsEncoder(true)
+            .setProfileLevels(AVC_PROFILE_LEVELS)
+            .setColorFormats(AVC_COLOR_FORMATS)
+            .build();
+
+    assertThat(codecCapabilities.getVideoCapabilities()).isNotNull();
+    assertThat(codecCapabilities.getVideoCapabilities().getSupportedWidths())
+        .isEqualTo(new Range<>(1, WIDTH));
+    assertThat(codecCapabilities.getVideoCapabilities().getSupportedHeights())
+        .isEqualTo(new Range<>(1, HEIGHT));
+  }
+
+  @Test
+  @Config(minSdk = Q)
   public void canCreateVideoDecoderCapabilities() {
     CodecCapabilities codecCapabilities =
         MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
@@ -353,4 +383,24 @@
     }
     return mediaFormat;
   }
+
+  /**
+   * Create a sample {@link MediaFormat}.
+   *
+   * @param mime one of MIMETYPE_* from {@link MediaFormat}.
+   * @param width The width of the content (in pixels).
+   * @param height The height of the content (in pixels).
+   * @param features an array of CodecCapabilities.FEATURE_ features to be enabled.
+   */
+  private static MediaFormat createMediaFormat(
+      String mime, int width, int height, String[] features) {
+    MediaFormat mediaFormat = new MediaFormat();
+    mediaFormat.setString(MediaFormat.KEY_MIME, mime);
+    mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width);
+    mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height);
+    for (String feature : features) {
+      mediaFormat.setFeatureEnabled(feature, true);
+    }
+    return mediaFormat;
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
index 9ecb63d..5de84d8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAssetManagerTest.java
@@ -1,8 +1,8 @@
 package org.robolectric.shadows;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.robolectric.shadows.ShadowAssetManager.legacyShadowOf;
@@ -46,7 +46,7 @@
 
   @Test
   public void openFd_shouldProvideFileDescriptorForDeflatedAsset() throws Exception {
-    assumeTrue(!useLegacy());
+    assume().that(useLegacy()).isFalse();
     expectedException.expect(FileNotFoundException.class);
     expectedException.expectMessage(
         "This file can not be opened as a file descriptor; it is probably compressed");
@@ -79,7 +79,7 @@
 
   @Test
   public void openNonAssetShouldThrowExceptionWhenFileDoesNotExist() throws IOException {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
 
     expectedException.expect(IOException.class);
     expectedException.expectMessage(
@@ -90,7 +90,7 @@
 
   @Test
   public void unknownResourceIdsShouldReportPackagesSearched() throws IOException {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
 
     expectedException.expect(Resources.NotFoundException.class);
     expectedException.expectMessage("Resource ID #0xffffffff");
@@ -102,7 +102,7 @@
   @Test
   public void forSystemResources_unknownResourceIdsShouldReportPackagesSearched()
       throws IOException {
-    if (!useLegacy()) return;
+    assume().that(useLegacy()).isTrue();
     expectedException.expect(Resources.NotFoundException.class);
     expectedException.expectMessage("Resource ID #0xffffffff");
 
@@ -113,8 +113,7 @@
   @Test
   @Config(qualifiers = "mdpi")
   public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierMdpi() throws IOException {
-    if (!useLegacy()) return;
-
+    assume().that(useLegacy()).isTrue();
     InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0);
     assertThat(countBytes(inputStream)).isEqualTo(8141);
   }
@@ -122,8 +121,7 @@
   @Test
   @Config(qualifiers = "hdpi")
   public void openNonAssetShouldOpenCorrectAssetBasedOnQualifierHdpi() throws IOException {
-    if (!useLegacy()) return;
-
+    assume().that(useLegacy()).isTrue();
     InputStream inputStream = assetManager.openNonAsset(0, "res/drawable/robolectric.png", 0);
     assertThat(countBytes(inputStream)).isEqualTo(23447);
   }
@@ -178,8 +176,7 @@
 
   @Test
   public void attrsToTypedArray_shouldAllowMockedAttributeSets() {
-    if (!useLegacy()) return;
-
+    assume().that(useLegacy()).isTrue();
     AttributeSet mockAttributeSet = mock(AttributeSet.class);
     when(mockAttributeSet.getAttributeCount()).thenReturn(1);
     when(mockAttributeSet.getAttributeNameResource(0)).thenReturn(android.R.attr.windowBackground);
@@ -191,7 +188,7 @@
 
   @Test
   public void whenStyleAttrResolutionFails_attrsToTypedArray_returnsNiceErrorMessage() {
-    if (!useLegacy()) return;
+    assume().that(useLegacy()).isTrue();
     expectedException.expect(RuntimeException.class);
     expectedException.expectMessage(
         "no value for org.robolectric:attr/styleNotSpecifiedInAnyTheme in theme with applied"
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
index b798a74..1498562 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioManagerTest.java
@@ -8,6 +8,7 @@
 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 com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -24,6 +25,7 @@
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioRecordingConfiguration;
+import android.media.AudioSystem;
 import android.media.MediaRecorder.AudioSource;
 import android.media.audiopolicy.AudioPolicy;
 import androidx.test.core.app.ApplicationProvider;
@@ -48,6 +50,17 @@
   private Context appContext;
   private AudioManager audioManager;
 
+  // When creating Audio Device Info, we need to pass external device type instead of internal input
+  // device(e.g. AudioDeviceInfo.TYPE_BLUETOOTH_SCO)
+  // The mapping between external device type and internal input device is:
+  // http://shortn/_7pV0nML4Cr
+  // Copied from
+  // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=989
+  private static final int DEVICE_OUT_BLUETOOTH_SCO = 0x10;
+  // Copied from
+  // http://cs/android-internal/frameworks/base/media/java/android/media/AudioSystem.java;l=1000
+  private static final int DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+
   @Before
   public void setUp() {
     appContext = ApplicationProvider.getApplicationContext();
@@ -403,7 +416,7 @@
   @Config(minSdk = M)
   public void registerAudioDeviceCallback_availableDevices_onAudioDevicesAddedCallback()
       throws Exception {
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
 
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
@@ -419,7 +432,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
 
     verifyNoMoreInteractions(callback);
@@ -434,7 +447,7 @@
     audioManager.unregisterAudioDeviceCallback(callback);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
 
     verifyNoMoreInteractions(callback);
@@ -448,7 +461,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
 
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
@@ -463,7 +476,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
 
     verifyNoMoreInteractions(callback);
@@ -476,7 +489,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).addInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -492,7 +505,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -508,7 +521,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setInputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
@@ -524,7 +537,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).removeInputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
 
     verifyNoMoreInteractions(callback);
@@ -537,7 +550,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
 
     verifyNoMoreInteractions(callback);
@@ -551,7 +564,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
 
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
@@ -566,7 +579,7 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
 
     verifyNoMoreInteractions(callback);
@@ -579,7 +592,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).addOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -595,7 +608,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
@@ -611,7 +624,7 @@
     AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setOutputDevices(Collections.singletonList(device));
 
     shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ false);
@@ -627,17 +640,130 @@
     audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
     verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
 
-    AudioDeviceInfo device = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).removeOutputDevice(device, /* notifyAudioDeviceCallbacks= */ true);
 
     verifyNoMoreInteractions(callback);
   }
 
   @Test
+  @Config(minSdk = S)
+  public void setAvailableCommunicationDevices_withCallbackRegistered_noNotificationCallback()
+      throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      addAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_deviceAddedAndNotifiesCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager)
+        .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      addAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager)
+        .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      addAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager)
+        .addAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      removeAvailableCommunicationDevice_withCallbackRegisteredAndDevicePresent_deviceRemovedAndNotifiesCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager)
+        .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ true);
+
+    verify(callback).onAudioDevicesRemoved(new AudioDeviceInfo[] {device});
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      removeAvailableCommunicationDeviceNoCallbackNotification_withCallbackRegisteredAndDevicePresent_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager).setAvailableCommunicationDevices(Collections.singletonList(device));
+
+    shadowOf(audioManager)
+        .removeAvailableCommunicationDevice(device, /* notifyAudioDeviceCallbacks= */ false);
+
+    verifyNoMoreInteractions(callback);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      removeAvailableCommunicationDevice_withCallbackRegisteredAndNoDevice_noNotificationCallback()
+          throws Exception {
+    AudioDeviceCallback callback = mock(AudioDeviceCallback.class);
+    audioManager.registerAudioDeviceCallback(callback, /* handler= */ null);
+    verify(callback).onAudioDevicesAdded(new AudioDeviceInfo[] {}); // initial registration
+
+    AudioDeviceInfo device = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    shadowOf(audioManager)
+        .removeAvailableCommunicationDevice(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);
+    AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
     shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
     shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
 
@@ -648,8 +774,8 @@
   @Test
   @Config(minSdk = M)
   public void getDevices_criteriaOutputs_getsAllOutputDevices() throws Exception {
-    AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
-    AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+    AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
     shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
     shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
 
@@ -660,8 +786,8 @@
   @Test
   @Config(minSdk = M)
   public void getDevices_criteriaInputsAndOutputs_getsAllDevices() throws Exception {
-    AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
-    AudioDeviceInfo a2dpDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
+    AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
+    AudioDeviceInfo a2dpDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_A2DP);
     shadowOf(audioManager).setInputDevices(ImmutableList.of(scoDevice));
     shadowOf(audioManager).setOutputDevices(ImmutableList.of(a2dpDevice));
 
@@ -672,7 +798,7 @@
   @Test
   @Config(minSdk = S)
   public void setCommunicationDevice_updatesCommunicationDevice() throws Exception {
-    AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setCommunicationDevice(scoDevice);
 
     assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice);
@@ -681,7 +807,7 @@
   @Test
   @Config(minSdk = S)
   public void clearCommunicationDevice_clearsCommunicationDevice() throws Exception {
-    AudioDeviceInfo scoDevice = createAudioDevice(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
+    AudioDeviceInfo scoDevice = createAudioDevice(DEVICE_OUT_BLUETOOTH_SCO);
     shadowOf(audioManager).setCommunicationDevice(scoDevice);
     assertThat(audioManager.getCommunicationDevice()).isEqualTo(scoDevice);
 
@@ -991,6 +1117,190 @@
     assertThat(audioSessionId).isNotEqualTo(audioSessionId2);
   }
 
+  @Test
+  @Config(minSdk = Q)
+  public void isOffloadSupported_withoutSupport() {
+    assertThat(
+            AudioManager.isOffloadedPlaybackSupported(
+                new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build(),
+                new AudioAttributes.Builder().build()))
+        .isFalse();
+  }
+
+  @Test
+  @Config(minSdk = Q, maxSdk = R)
+  public void isOffloadSupported_withSetOffloadSupported() {
+    AudioFormat format =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+    ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = Q, maxSdk = R)
+  public void isOffloadSupported_withSetOffloadSupportedAddedAndRemoved() {
+    AudioFormat format =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    ShadowAudioSystem.setOffloadSupported(format, attributes, true);
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+
+    ShadowAudioSystem.setOffloadSupported(format, attributes, false);
+
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void isOffloadSupported_withSetOffloadPlaybackSupport() {
+    AudioFormat format =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isFalse();
+
+    ShadowAudioSystem.setOffloadPlaybackSupport(format, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+    assertThat(AudioManager.isOffloadedPlaybackSupported(format, attributes)).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void getPlaybackOffloadSupport_withSetOffloadSupport_returnsOffloadSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setSampleRate(48_000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+            .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+            .build();
+    AudioAttributes audioAttributes =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    ShadowAudioSystem.setOffloadPlaybackSupport(
+        audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+    int playbackOffloadSupport =
+        AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+    assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void
+      getPlaybackOffloadSupport_withoutSetDirectPlaybackSupport_returnsOffloadNotSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setSampleRate(48_000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+            .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+            .build();
+    AudioAttributes audioAttributes =
+        new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+
+    int playbackOffloadSupport =
+        AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes);
+
+    assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+  }
+
+  @Test
+  @Config(minSdk = S)
+  public void getPlaybackOffloadSupport_withSameAudioAttrUsage_returnsOffloadSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setSampleRate(48_000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+            .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+            .build();
+    AudioAttributes audioAttributes =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    ShadowAudioSystem.setOffloadPlaybackSupport(
+        audioFormat, audioAttributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+    AudioAttributes audioAttributes2 =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    int playbackOffloadSupport =
+        AudioManager.getPlaybackOffloadSupport(audioFormat, audioAttributes2);
+
+    assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.OFFLOAD_SUPPORTED);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getDirectPlaybackSupport_withSetDirectPlaybackSupport_returnsOffloadSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setSampleRate(48_000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+            .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+            .build();
+    AudioAttributes audioAttributes =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    ShadowAudioSystem.setDirectPlaybackSupport(
+        audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+
+    int playbackOffloadSupport =
+        AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+    assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getDirectPlaybackSupport_withShadowAudioSystemReset_returnsOffloadNotSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setSampleRate(48_000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
+            .setEncoding(AudioFormat.ENCODING_AAC_HE_V2)
+            .build();
+    AudioAttributes audioAttributes =
+        new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+            .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+    ShadowAudioSystem.setDirectPlaybackSupport(
+        audioFormat, audioAttributes, AudioSystem.DIRECT_OFFLOAD_SUPPORTED);
+    ShadowAudioSystem.reset();
+
+    int playbackOffloadSupport =
+        AudioManager.getDirectPlaybackSupport(audioFormat, audioAttributes);
+
+    assertThat(playbackOffloadSupport).isEqualTo(AudioSystem.DIRECT_NOT_SUPPORTED);
+  }
+
   private static AudioDeviceInfo createAudioDevice(int type) throws ReflectiveOperationException {
     AudioDeviceInfo info = Shadow.newInstanceOf(AudioDeviceInfo.class);
     Field portField = AudioDeviceInfo.class.getDeclaredField("mPort");
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
index adffa01..e8869bd 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAudioTrackTest.java
@@ -4,13 +4,20 @@
 import static android.media.AudioTrack.WRITE_BLOCKING;
 import static android.media.AudioTrack.WRITE_NON_BLOCKING;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static android.os.Build.VERSION_CODES.M;
 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
 
 import android.media.AudioAttributes;
 import android.media.AudioFormat;
 import android.media.AudioManager;
+import android.media.AudioSystem;
 import android.media.AudioTrack;
+import android.media.PlaybackParams;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import java.nio.ByteBuffer;
 import org.junit.Test;
@@ -172,6 +179,360 @@
     assertThat(written).isEqualTo(ERROR_BAD_VALUE);
   }
 
+  @Test
+  @Config(minSdk = M)
+  public void getPlaybackParams_withSetPlaybackParams_returnsSetPlaybackParams() {
+    PlaybackParams playbackParams =
+        new PlaybackParams()
+            .allowDefaults()
+            .setSpeed(1.0f)
+            .setPitch(1.0f)
+            .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
+    AudioTrack audioTrack = getSampleAudioTrack();
+    audioTrack.setPlaybackParams(playbackParams);
+
+    assertThat(audioTrack.getPlaybackParams()).isEqualTo(playbackParams);
+  }
+
+  @Test
+  public void addDirectPlaybackSupport_forPcmEncoding_throws() {
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ShadowAudioTrack.addDirectPlaybackSupport(
+                getAudioFormat(AudioFormat.ENCODING_PCM_8BIT), attributes));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ShadowAudioTrack.addDirectPlaybackSupport(
+                getAudioFormat(AudioFormat.ENCODING_PCM_16BIT), attributes));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ShadowAudioTrack.addDirectPlaybackSupport(
+                getAudioFormat(AudioFormat.ENCODING_PCM_24BIT_PACKED), attributes));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ShadowAudioTrack.addDirectPlaybackSupport(
+                getAudioFormat(AudioFormat.ENCODING_PCM_32BIT), attributes));
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ShadowAudioTrack.addDirectPlaybackSupport(
+                getAudioFormat(AudioFormat.ENCODING_PCM_FLOAT), attributes));
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void isDirectPlaybackSupported() {
+    AudioFormat ac3Format = getAudioFormat(AudioFormat.ENCODING_AC3);
+    AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+    assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+
+    ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+    assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void isDirectPlaybackSupported_differentFormatOrAttributeFields() {
+    AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+    AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+
+    ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+
+    assertThat(
+            AudioTrack.isDirectPlaybackSupported(
+                new AudioFormat.Builder()
+                    .setEncoding(AudioFormat.ENCODING_AC3)
+                    .setSampleRate(65000)
+                    .build(),
+                audioAttributes))
+        .isFalse();
+    assertThat(
+            AudioTrack.isDirectPlaybackSupported(
+                ac3Format,
+                new AudioAttributes.Builder()
+                    .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+                    .build()))
+        .isFalse();
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void clearDirectPlaybackSupportedEncodings() {
+    AudioFormat ac3Format = new AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_AC3).build();
+    AudioAttributes audioAttributes = new AudioAttributes.Builder().build();
+    ShadowAudioTrack.addDirectPlaybackSupport(ac3Format, audioAttributes);
+    assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isTrue();
+
+    ShadowAudioTrack.clearDirectPlaybackSupportedFormats();
+
+    assertThat(AudioTrack.isDirectPlaybackSupported(ac3Format, audioAttributes)).isFalse();
+  }
+
+  @Test
+  public void addAllowedNonPcmEncoding_forPcmEncoding_throws() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_8BIT));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_16BIT));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_24BIT_PACKED));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_32BIT));
+    assertThrows(
+        IllegalArgumentException.class,
+        () -> ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_PCM_FLOAT));
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void createInstance_withNonPcmEncodingNotAllowed_throws() {
+    assertThrows(
+        UnsupportedOperationException.class,
+        () ->
+            new AudioTrack.Builder()
+                .setAudioFormat(
+                    new AudioFormat.Builder()
+                        .setEncoding(AudioFormat.ENCODING_AC3)
+                        .setSampleRate(48000)
+                        .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+                        .build())
+                .setBufferSizeInBytes(65536)
+                .build());
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void createInstance_withNonPcmEncodingAllowed() {
+    ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+    new AudioTrack.Builder()
+        .setAudioFormat(
+            new AudioFormat.Builder()
+                .setEncoding(AudioFormat.ENCODING_AC3)
+                .setSampleRate(48000)
+                .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+                .build())
+        .setBufferSizeInBytes(65536)
+        .build();
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void createInstance_withOffloadAndEncodingNotOffloaded_throws() {
+    assertThrows(
+        UnsupportedOperationException.class,
+        () ->
+            new AudioTrack.Builder()
+                .setAudioFormat(
+                    new AudioFormat.Builder()
+                        .setEncoding(AudioFormat.ENCODING_AC3)
+                        .setSampleRate(48000)
+                        .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+                        .build())
+                .setBufferSizeInBytes(65536)
+                .setOffloadedPlayback(true)
+                .build());
+  }
+
+  @Test
+  @Config(minSdk = Q, maxSdk = R)
+  public void createInstance_withOffloadAndEncodingIsOffloadSupported() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    ShadowAudioSystem.setOffloadSupported(audioFormat, attributes, /* supported= */ true);
+
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(audioFormat)
+            .setAudioAttributes(attributes)
+            .setBufferSizeInBytes(65536)
+            .setOffloadedPlayback(true)
+            .build();
+
+    assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+  }
+
+  @Test
+  @Config(sdk = S)
+  public void createInstance_withOffloadAndGetOffloadSupport() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    ShadowAudioSystem.setOffloadPlaybackSupport(
+        audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(audioFormat)
+            .setAudioAttributes(attributes)
+            .setBufferSizeInBytes(65536)
+            .setOffloadedPlayback(true)
+            .build();
+
+    assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void createInstance_withOffloadAndGetDirectPlaybackSupport() {
+    AudioFormat audioFormat =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    ShadowAudioSystem.setDirectPlaybackSupport(
+        audioFormat, attributes, AudioSystem.OFFLOAD_SUPPORTED);
+
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(audioFormat)
+            .setAudioAttributes(attributes)
+            .setBufferSizeInBytes(65536)
+            .setOffloadedPlayback(true)
+            .build();
+
+    assertThat(audioTrack.isOffloadedPlayback()).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void clearAllowedNonPcmEncodings() {
+    AudioFormat surroundAudioFormat =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+    new AudioTrack.Builder()
+        .setAudioFormat(surroundAudioFormat)
+        .setBufferSizeInBytes(65536)
+        .build();
+
+    ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+    assertThrows(
+        UnsupportedOperationException.class,
+        () ->
+            new AudioTrack.Builder()
+                .setAudioFormat(surroundAudioFormat)
+                .setBufferSizeInBytes(65536)
+                .build());
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void write_withNonPcmEncodingSupported_succeeds() {
+    ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(
+                new AudioFormat.Builder()
+                    .setEncoding(AudioFormat.ENCODING_AC3)
+                    .setSampleRate(48000)
+                    .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+                    .build())
+            .setAudioAttributes(new AudioAttributes.Builder().build())
+            .setBufferSizeInBytes(32 * 1024)
+            .build();
+
+    assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+    assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+        .isEqualTo(128);
+  }
+
+  @Test
+  @Config(minSdk = Q, maxSdk = R)
+  public void write_withOffloadUntilApi30_succeeds() {
+    ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+    AudioFormat ac3Format =
+        new AudioFormat.Builder()
+            .setEncoding(AudioFormat.ENCODING_AC3)
+            .setSampleRate(48000)
+            .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+            .build();
+    AudioAttributes attributes = new AudioAttributes.Builder().build();
+    ShadowAudioSystem.setOffloadSupported(ac3Format, attributes, /* supported= */ true);
+
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(ac3Format)
+            .setAudioAttributes(new AudioAttributes.Builder().build())
+            .setBufferSizeInBytes(32 * 1024)
+            .setOffloadedPlayback(true)
+            .build();
+
+    assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(128);
+    assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING)).isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+        .isEqualTo(128);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+        .isEqualTo(128);
+  }
+
+  @Test
+  @Config(minSdk = Q)
+  public void write_withNonPcmEncodingNoLongerSupported_returnsErrorDeadObject() {
+    ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_AC3);
+    AudioTrack audioTrack =
+        new AudioTrack.Builder()
+            .setAudioFormat(
+                new AudioFormat.Builder()
+                    .setEncoding(AudioFormat.ENCODING_AC3)
+                    .setSampleRate(48000)
+                    .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
+                    .build())
+            .setAudioAttributes(new AudioAttributes.Builder().build())
+            .setBufferSizeInBytes(32 * 1024)
+            .build();
+
+    ShadowAudioTrack.clearAllowedNonPcmEncodings();
+
+    assertThat(audioTrack.write(new byte[128], 0, 128)).isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+    assertThat(audioTrack.write(new byte[128], 0, 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+    assertThat(audioTrack.write(ByteBuffer.allocate(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING))
+        .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+    assertThat(audioTrack.write(ByteBuffer.allocateDirect(128), 128, AudioTrack.WRITE_BLOCKING, 0L))
+        .isEqualTo(AudioTrack.ERROR_DEAD_OBJECT);
+  }
+
   @Override
   @Config(minSdk = Q)
   public void onAudioDataWritten(
@@ -195,4 +556,8 @@
                 .build())
         .build();
   }
+
+  private AudioFormat getAudioFormat(int encoding) {
+    return new AudioFormat.Builder().setEncoding(encoding).build();
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
index caed178..78b9edb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothGattTest.java
@@ -31,6 +31,7 @@
   private static final String ACTION_DISCOVER = "DISCOVER";
   private static final String ACTION_READ = "READ";
   private static final String ACTION_WRITE = "WRITE";
+  private static final String REMOTE_ADDRESS = "R-A";
 
   private int resultStatus = INITIAL_VALUE;
   private int resultState = INITIAL_VALUE;
@@ -274,6 +275,15 @@
 
   @Test
   @Config(minSdk = O)
+  public void getService_afterAddService() {
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    assertThat(bluetoothGatt.discoverServices()).isFalse();
+    assertThat(bluetoothGatt.getService(service1.getUuid())).isEqualTo(service1);
+    assertThat(bluetoothGatt.getService(service2.getUuid())).isNull();
+  }
+
+  @Test
+  @Config(minSdk = O)
   public void discoverServices_clearsService() {
     shadowOf(bluetoothGatt).setGattCallback(callback);
     shadowOf(bluetoothGatt).addDiscoverableService(service1);
@@ -471,4 +481,103 @@
     assertThat(resultCharacteristic).isEqualTo(characteristic);
     assertThat(shadowOf(bluetoothGatt).getLatestWrittenBytes()).isEqualTo(CHARACTERISTIC_VALUE);
   }
+
+  @Test
+  public void test_getBluetoothConnectionManager() {
+    assertThat(shadowOf(bluetoothGatt).getBluetoothConnectionManager()).isNotNull();
+  }
+
+  @Test
+  public void test_notifyConnection_connects() {
+    shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+    assertThat(
+            shadowOf(bluetoothGatt)
+                .getBluetoothConnectionManager()
+                .hasGattClientConnection(REMOTE_ADDRESS))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultState).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  public void test_notifyConnection_connectsWithCallbackSet() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isTrue();
+    assertThat(
+            shadowOf(bluetoothGatt)
+                .getBluetoothConnectionManager()
+                .hasGattClientConnection(REMOTE_ADDRESS))
+        .isTrue();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultState).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+  }
+
+  @Test
+  public void test_notifyDisconnection_disconnects() {
+    shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(
+            shadowOf(bluetoothGatt)
+                .getBluetoothConnectionManager()
+                .hasGattClientConnection(REMOTE_ADDRESS))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultState).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  public void test_notifyDisconnection_disconnectsWithCallbackSet() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(
+            shadowOf(bluetoothGatt)
+                .getBluetoothConnectionManager()
+                .hasGattClientConnection(REMOTE_ADDRESS))
+        .isFalse();
+    assertThat(resultStatus).isEqualTo(INITIAL_VALUE);
+    assertThat(resultState).isEqualTo(INITIAL_VALUE);
+    assertThat(resultAction).isNull();
+  }
+
+  @Test
+  public void test_notifyDisconnection_disconnectsWithCallbackSet_connectedInitially() {
+    shadowOf(bluetoothGatt).setGattCallback(callback);
+    shadowOf(bluetoothGatt).notifyConnection(REMOTE_ADDRESS);
+    shadowOf(bluetoothGatt).notifyDisconnection(REMOTE_ADDRESS);
+    assertThat(
+            shadowOf(bluetoothGatt)
+                .getBluetoothConnectionManager()
+                .hasGattClientConnection(REMOTE_ADDRESS))
+        .isFalse();
+    assertThat(shadowOf(bluetoothGatt).isConnected()).isFalse();
+    assertThat(resultStatus).isEqualTo(BluetoothGatt.GATT_SUCCESS);
+    assertThat(resultState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    assertThat(resultAction).isEqualTo(ACTION_CONNECTION);
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void allowCharacteristicNotification_canSetNotification() {
+    service1.addCharacteristic(characteristicWithReadProperty);
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    shadowOf(bluetoothGatt).allowCharacteristicNotification(characteristicWithReadProperty);
+    assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+        .isTrue();
+  }
+
+  @Test
+  @Config(minSdk = O)
+  public void disallowCharacteristicNotification_cannotSetNotification() {
+    service1.addCharacteristic(characteristicWithReadProperty);
+    shadowOf(bluetoothGatt).addDiscoverableService(service1);
+    shadowOf(bluetoothGatt).disallowCharacteristicNotification(characteristicWithReadProperty);
+    assertThat(bluetoothGatt.setCharacteristicNotification(characteristicWithReadProperty, true))
+        .isFalse();
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
index 9482ba8..cae11d9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowBluetoothHeadsetTest.java
@@ -22,9 +22,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
@@ -37,8 +35,6 @@
   private BluetoothHeadset bluetoothHeadset;
   private Application context;
 
-  @Rule public ExpectedException thrown = ExpectedException.none();
-
   @Before
   public void setUp() throws Exception {
     device1 = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:AA:BB");
@@ -61,6 +57,41 @@
   }
 
   @Test
+  public void getConnectedDevices_doesNotReturnDevicesInNonConnectedStates() {
+    shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+    shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+    assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+  }
+
+  @Test
+  public void getConnectionState_returnsStoredConnectionState() {
+    shadowOf(bluetoothHeadset).addDevice(device1, BluetoothProfile.STATE_CONNECTING);
+    shadowOf(bluetoothHeadset).addDevice(device2, BluetoothProfile.STATE_DISCONNECTING);
+
+    assertThat(bluetoothHeadset.getConnectionState(device1))
+        .isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    assertThat(bluetoothHeadset.getConnectionState(device2))
+        .isEqualTo(BluetoothProfile.STATE_DISCONNECTING);
+  }
+
+  @Test
+  public void removeDevice_getConnectionStateReturnsDisconnected() {
+    shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+    shadowOf(bluetoothHeadset).removeDevice(device1);
+
+    assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+  }
+
+  @Test
+  public void removeDevice_getConnectedDevicesReturnsEmpty() {
+    shadowOf(bluetoothHeadset).addConnectedDevice(device1);
+    shadowOf(bluetoothHeadset).removeDevice(device1);
+
+    assertThat(bluetoothHeadset.getConnectedDevices()).isEmpty();
+  }
+
+  @Test
   public void getConnectionState_defaultsToDisconnected() {
     shadowOf(bluetoothHeadset).addConnectedDevice(device1);
     shadowOf(bluetoothHeadset).addConnectedDevice(device2);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
index 02b149d..cf3eba8 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowContextWrapperTest.java
@@ -4,6 +4,7 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION_CODES.KITKAT;
 import static android.os.Build.VERSION_CODES.M;
+import static android.os.Build.VERSION_CODES.P;
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
@@ -46,6 +47,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.ConfigTestReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithEmptyActionReceiver;
+import org.robolectric.CustomConstructorReceiverWrapper.CustomConstructorWithOneActionReceiver;
 import org.robolectric.R;
 import org.robolectric.Robolectric;
 import org.robolectric.RuntimeEnvironment;
@@ -95,6 +98,20 @@
   }
 
   @Test
+  @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
+  public void registerReceiver_shouldGetReceiverWithCustomConstructorEmptyAction() {
+    BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithEmptyActionReceiver.class);
+    assertThat(receiver).isInstanceOf(CustomConstructorWithEmptyActionReceiver.class);
+  }
+
+  @Test
+  @Config(manifest = "TestAndroidManifestWithAppComponentFactory.xml", minSdk = P)
+  public void registerReceiver_shouldGetReceiverWithCustomConstructorAndOneAction() {
+    BroadcastReceiver receiver = getReceiverOfClass(CustomConstructorWithOneActionReceiver.class);
+    assertThat(receiver).isInstanceOf(CustomConstructorWithOneActionReceiver.class);
+  }
+
+  @Test
   public void registerReceiver_shouldRegisterForAllIntentFilterActions() throws Exception {
     BroadcastReceiver receiver = broadcastReceiver("Larry");
     contextWrapper.registerReceiver(receiver, intentFilter("foo", "baz"));
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
index 8ee669a..8000cc6 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowInputManagerTest.java
@@ -1,10 +1,14 @@
 package org.robolectric.shadows;
 
 import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 import android.hardware.input.InputManager;
+import android.hardware.input.InputManager.InputDeviceListener;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.MotionEvent;
 import android.view.VerifiedMotionEvent;
 import androidx.test.core.app.ApplicationProvider;
@@ -16,7 +20,7 @@
 
 /** Unit tests for {@link ShadowInputManager}. */
 @RunWith(AndroidJUnit4.class)
-@Config(minSdk = R)
+@Config(minSdk = R, maxSdk = TIRAMISU)
 public class ShadowInputManagerTest {
   private InputManager inputManager;
 
@@ -38,4 +42,22 @@
     assertThat(verifiedMotionEvent.getEventTimeNanos()).isEqualTo(23456000000L);
     assertThat(verifiedMotionEvent.getDownTimeNanos()).isEqualTo(12345000000L);
   }
+
+  static class InputDeviceListenerNoOp implements InputDeviceListener {
+    @Override
+    public void onInputDeviceAdded(int deviceId) {}
+
+    @Override
+    public void onInputDeviceRemoved(int deviceId) {}
+
+    @Override
+    public void onInputDeviceChanged(int deviceId) {}
+  }
+
+  @Test
+  public void testRegisterInputDeviceListener_doesNotCrash() {
+    InputDeviceListenerNoOp listener = new InputDeviceListenerNoOp();
+    inputManager.registerInputDeviceListener(listener, new Handler(Looper.getMainLooper()));
+    inputManager.unregisterInputDeviceListener(listener);
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
index edff632..323cb57 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowLauncherAppsTest.java
@@ -264,6 +264,20 @@
   }
 
   @Test
+  @Config(minSdk = L)
+  public void testIsActivityEnabled() {
+    ComponentName c1 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity1");
+    ComponentName c2 = new ComponentName(ApplicationProvider.getApplicationContext(), "Activity2");
+    ComponentName c3 = new ComponentName("other", "Activity1");
+    assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isFalse();
+
+    shadowOf(launcherApps).setActivityEnabled(USER_HANDLE, c1);
+    assertThat(launcherApps.isActivityEnabled(c1, USER_HANDLE)).isTrue();
+    assertThat(launcherApps.isActivityEnabled(c2, USER_HANDLE)).isFalse();
+    assertThat(launcherApps.isActivityEnabled(c3, USER_HANDLE)).isFalse();
+  }
+
+  @Test
   @Config(minSdk = O)
   public void testGetApplicationInfo_packageNotFound() throws Exception {
     Throwable throwable =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
index 6ae57f9..7744b95 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPaintTest.java
@@ -71,6 +71,15 @@
   }
 
   @Test
+  public void shouldSetStrikeThruText() {
+    Paint paint = new Paint();
+    paint.setStrikeThruText(true);
+    assertThat(paint.isStrikeThruText()).isTrue();
+    paint.setStrikeThruText(false);
+    assertThat(paint.isStrikeThruText()).isFalse();
+  }
+
+  @Test
   public void measureTextActuallyMeasuresLength() {
     Paint paint = new Paint();
     assertThat(paint.measureText("Hello")).isEqualTo(5.0f);
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
index cadc3e9..71e85c9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowPausedLooperTest.java
@@ -4,6 +4,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.concurrent.Executors.newSingleThreadExecutor;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
@@ -13,6 +14,7 @@
 import static org.robolectric.Shadows.shadowOf;
 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
 
+import android.os.Build.VERSION_CODES;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -32,6 +34,7 @@
 import org.junit.Test;
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.res.android.Ref;
 import org.robolectric.shadow.api.Shadow;
@@ -526,10 +529,8 @@
   }
 
   @Test
-  public void testIdleNotStuck_whenThreadCrashes() throws Exception {
-    HandlerThread thread = new HandlerThread("WillCrash");
-    thread.start();
-    Looper looper = thread.getLooper();
+  public void idle_looperPaused_idleHandlerThrowsException() throws Exception {
+    Looper looper = handlerThread.getLooper();
     shadowOf(looper).pause();
     new Handler(looper)
         .post(
@@ -537,12 +538,69 @@
               Looper.myQueue()
                   .addIdleHandler(
                       () -> {
-                        throw new RuntimeException();
+                        throw new IllegalStateException();
                       });
             });
-    shadowOf(looper).idle();
-    thread.join(5_000);
-    assertThat(thread.getState()).isEqualTo(Thread.State.TERMINATED);
+    assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+    handlerThread.join(5_000);
+    assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+  }
+
+  @Test
+  public void idle_looperPaused_runnableThrowsException() throws Exception {
+    Looper looper = handlerThread.getLooper();
+    shadowOf(looper).pause();
+    new Handler(looper)
+        .post(
+            () -> {
+              throw new IllegalStateException();
+            });
+
+    assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+    handlerThread.join(5_000);
+    assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+  }
+
+  @Test
+  public void idle_looperRunning_runnableThrowsException() throws Exception {
+    Looper looper = handlerThread.getLooper();
+    new Handler(looper)
+        .post(
+            () -> {
+              throw new IllegalStateException();
+            });
+
+    assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
+    handlerThread.join(5_000);
+    assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+  }
+
+  @Test
+  public void post_throws_if_looper_died() throws Exception {
+    Looper looper = handlerThread.getLooper();
+    new Handler(looper)
+        .post(
+            () -> {
+              throw new IllegalStateException();
+            });
+    handlerThread.join(5_000);
+    assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+    assertThrows(IllegalStateException.class, () -> new Handler(looper).post(() -> {}));
+  }
+
+  @Test
+  public void idle_throws_if_looper_died() throws Exception {
+    Looper looper = handlerThread.getLooper();
+    new Handler(looper)
+        .post(
+            () -> {
+              throw new IllegalStateException();
+            });
+    handlerThread.join(5_000);
+    assertThat(handlerThread.getState()).isEqualTo(Thread.State.TERMINATED);
+
+    assertThrows(IllegalStateException.class, () -> shadowOf(looper).idle());
   }
 
   @Test
@@ -565,6 +623,43 @@
     assertThat(foregroundThreadReceived.get()).isTrue();
   }
 
+  @Test
+  @Config(minSdk = VERSION_CODES.M)
+  public void runOneTask_ignoreSyncBarrier() {
+    int barrier = Looper.getMainLooper().getQueue().postSyncBarrier();
+
+    final AtomicBoolean wasRun = new AtomicBoolean(false);
+    new Handler(Looper.getMainLooper()).post(() -> wasRun.set(true));
+
+    ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper());
+    shadowPausedLooper.runOneTask();
+
+    // tasks should not be executed when blocked by a sync barrier
+    assertThat(wasRun.get()).isFalse();
+    // sync barrier will throw if the barrier was not found.
+    Looper.getMainLooper().getQueue().removeSyncBarrier(barrier);
+
+    shadowPausedLooper.runOneTask();
+    assertThat(wasRun.get()).isTrue();
+  }
+
+  @Test
+  @Config(minSdk = VERSION_CODES.P)
+  public void runOneTask_ignoreSyncBarrier_with_async() {
+    int barrier = Looper.getMainLooper().getQueue().postSyncBarrier();
+
+    final AtomicBoolean wasRun = new AtomicBoolean(false);
+    Handler.createAsync(Looper.getMainLooper()).post(() -> wasRun.set(true));
+
+    ShadowPausedLooper shadowPausedLooper = Shadow.extract(Looper.getMainLooper());
+    shadowPausedLooper.runOneTask();
+
+    // tasks should be executed as the handler is async
+    assertThat(wasRun.get()).isTrue();
+    // sync barrier will throw if the barrier was not found.
+    Looper.getMainLooper().getQueue().removeSyncBarrier(barrier);
+  }
+
   private static class BlockingRunnable implements Runnable {
     CountDownLatch latch = new CountDownLatch(1);
 
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
index da34401..a4732b9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowResourcesTest.java
@@ -2,8 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.N_MR1;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assume.assumeFalse;
-import static org.junit.Assume.assumeTrue;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.robolectric.Shadows.shadowOf;
 import static org.robolectric.shadows.ShadowAssetManager.useLegacy;
 
@@ -85,7 +84,7 @@
 
   @Test
   public void openRawResourceFd_shouldReturnsNullForLegacyResource() throws Exception {
-    assumeTrue(useLegacy());
+    assume().that(useLegacy()).isTrue();
     try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) {
         assertThat(afd).isNull();
     }
@@ -93,7 +92,7 @@
 
   @Test
   public void openRawResourceFd_shouldReturnsValidFdForUnCompressFile() throws Exception {
-    assumeFalse(useLegacy());
+    assume().that(useLegacy()).isFalse();
     try (AssetFileDescriptor afd = resources.openRawResourceFd(R.raw.raw_resource)) {
         assertThat(afd).isNotNull();
     }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
index 323511f..878777c 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSQLiteConnectionTest.java
@@ -3,8 +3,8 @@
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.robolectric.annotation.SQLiteMode.Mode.LEGACY;
 import static org.robolectric.shadows.ShadowLegacySQLiteConnection.convertSQLWithLocalizedUnicodeCollator;
 
@@ -64,7 +64,7 @@
 
   @Test
   public void testSqlConversion() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     assertThat(convertSQLWithLocalizedUnicodeCollator("select * from `routine`"))
         .isEqualTo("select * from `routine`");
 
@@ -88,7 +88,7 @@
 
   @Test
   public void testSQLWithLocalizedOrUnicodeCollatorShouldBeSortedAsNoCase() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
     database.execSQL("insert into routine(name) values ('Hand press 1')");
     database.execSQL("insert into routine(name) values ('hand press 2')");
@@ -116,28 +116,28 @@
 
   @Test
   public void nativeOpen_addsConnectionToPool() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     assertThat(conn).isNotNull();
     assertWithMessage("open").that(conn.isOpen()).isTrue();
   }
 
   @Test
   public void nativeClose_closesConnection() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     ShadowLegacySQLiteConnection.nativeClose(ptr);
     assertWithMessage("open").that(conn.isOpen()).isFalse();
   }
 
   @Test
   public void reset_closesConnection() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     ShadowLegacySQLiteConnection.reset();
     assertWithMessage("open").that(conn.isOpen()).isFalse();
   }
 
   @Test
   public void reset_clearsConnectionCache() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     final Map<Long, SQLiteConnection> connectionsMap =
         ReflectionHelpers.getField(connections, "connectionsMap");
 
@@ -149,7 +149,7 @@
 
   @Test
   public void reset_clearsStatementCache() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     final Map<Long, SQLiteStatement> statementsMap =
         ReflectionHelpers.getField(connections, "statementsMap");
 
@@ -161,7 +161,7 @@
 
   @Test
   public void error_resultsInSpecificExceptionWithCause() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     try {
       database.execSQL("insert into routine(name) values ('Hand press 1')");
       ContentValues values = new ContentValues(1);
@@ -178,7 +178,7 @@
 
   @Test
   public void interruption_doesNotConcurrentlyModifyDatabase() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     Thread.currentThread().interrupt();
     try {
       database.execSQL("insert into routine(name) values ('الصحافة اليدوية')");
@@ -190,7 +190,7 @@
 
   @Test
   public void test_setUseInMemoryDatabase() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     assertThat(conn.isMemoryDatabase()).isFalse();
     ShadowSQLiteConnection.setUseInMemoryDatabase(true);
     SQLiteDatabase inMemoryDb = createDatabase("in_memory.db");
@@ -201,7 +201,7 @@
 
   @Test
   public void cancel_shouldCancelAllStatements() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     SQLiteStatement statement1 =
         database.compileStatement("insert into routine(name) values ('Hand press 1')");
     SQLiteStatement statement2 =
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
index 4e7fb16..a527ec9 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowSubscriptionManagerTest.java
@@ -10,6 +10,8 @@
 import static org.junit.Assert.assertThrows;
 import static org.robolectric.Shadows.shadowOf;
 
+import android.os.Handler;
+import android.os.Looper;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -90,6 +92,29 @@
   }
 
   @Test
+  public void
+      addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldCallbackImmediately() {
+    DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+    shadowOf(subscriptionManager)
+        .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener);
+
+    assertThat(listener.subscriptionChangedCount).isEqualTo(1);
+  }
+
+  @Test
+  public void addOnSubscriptionsChangedListener_whenHasExecutorParameter_shouldAddListener() {
+    DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+    shadowOf(subscriptionManager)
+        .addOnSubscriptionsChangedListener(new Handler(Looper.getMainLooper())::post, listener);
+
+    shadowOf(subscriptionManager)
+        .setActiveSubscriptionInfos(
+            SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo());
+
+    assertThat(listener.subscriptionChangedCount).isEqualTo(2);
+  }
+
+  @Test
   public void removeOnSubscriptionsChangedListener_shouldRemoveListener() {
     DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
     DummySubscriptionsChangedListener listener2 = new DummySubscriptionsChangedListener();
@@ -106,6 +131,21 @@
   }
 
   @Test
+  public void hasOnSubscriptionsChangedListener_whenListenerNotExist_shouldReturnFalse() {
+    DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+
+    assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isFalse();
+  }
+
+  @Test
+  public void hasOnSubscriptionsChangedListener_whenListenerExist_shouldReturnTrue() {
+    DummySubscriptionsChangedListener listener = new DummySubscriptionsChangedListener();
+    shadowOf(subscriptionManager).addOnSubscriptionsChangedListener(listener);
+
+    assertThat(shadowOf(subscriptionManager).hasOnSubscriptionsChangedListener(listener)).isTrue();
+  }
+
+  @Test
   public void getActiveSubscriptionInfo_shouldReturnInfoWithSubId() {
     SubscriptionInfo expectedSubscriptionInfo =
         SubscriptionInfoBuilder.newBuilder().setId(123).buildSubscriptionInfo();
@@ -331,6 +371,48 @@
         .isEqualTo("123");
   }
 
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPhoneNumberWithSource_phoneNumberNotSet_returnsEmptyString() {
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+        .isEqualTo("");
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+        .isEqualTo("");
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+        .isEqualTo("");
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void getPhoneNumberWithSource_setPhoneNumber_returnsPhoneNumber() {
+    shadowOf(subscriptionManager)
+        .setPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "123");
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_UICC))
+        .isEqualTo("123");
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_CARRIER))
+        .isEqualTo("123");
+    assertThat(
+            subscriptionManager.getPhoneNumber(
+                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
+                SubscriptionManager.PHONE_NUMBER_SOURCE_IMS))
+        .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 8571627..ad3adeb 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowTelephonyManagerTest.java
@@ -27,6 +27,8 @@
 import static android.telephony.TelephonyManager.CALL_STATE_RINGING;
 import static android.telephony.TelephonyManager.NETWORK_TYPE_EVDO_0;
 import static android.telephony.TelephonyManager.NETWORK_TYPE_LTE;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_NUMBER_SOURCE_DATABASE;
+import static android.telephony.emergency.EmergencyNumber.EMERGENCY_SERVICE_CATEGORY_POLICE;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 import static org.junit.Assert.assertEquals;
@@ -74,10 +76,12 @@
 import android.telephony.TelephonyManager.CellInfoCallback;
 import android.telephony.UiccSlotInfo;
 import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.telephony.emergency.EmergencyNumber;
 import android.telephony.gba.UaSecurityProtocolIdentifier;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
@@ -374,6 +378,21 @@
   }
 
   @Test
+  @Config(minSdk = S)
+  public void shouldGiveCallStateForSubscription() {
+    PhoneStateListener listener = mock(PhoneStateListener.class);
+    telephonyManager.listen(listener, LISTEN_CALL_STATE);
+
+    shadowOf(telephonyManager).setCallState(CALL_STATE_RINGING, "911");
+    assertEquals(CALL_STATE_RINGING, telephonyManager.getCallStateForSubscription());
+    verify(listener).onCallStateChanged(CALL_STATE_RINGING, "911");
+
+    shadowOf(telephonyManager).setCallState(CALL_STATE_OFFHOOK, "911");
+    assertEquals(CALL_STATE_OFFHOOK, telephonyManager.getCallStateForSubscription());
+    verify(listener).onCallStateChanged(CALL_STATE_OFFHOOK, null);
+  }
+
+  @Test
   public void shouldGiveCallState() {
     PhoneStateListener listener = mock(PhoneStateListener.class);
     telephonyManager.listen(listener, LISTEN_CALL_STATE);
@@ -803,6 +822,24 @@
   }
 
   @Test
+  @Config(minSdk = S)
+  public void setDataEnabledForReasonChangesIsDataEnabledForReason() {
+    int correctReason = TelephonyManager.DATA_ENABLED_REASON_POLICY;
+    int incorrectReason = TelephonyManager.DATA_ENABLED_REASON_USER;
+
+    assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+    assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+    telephonyManager.setDataEnabledForReason(correctReason, false);
+    assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isFalse();
+    assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+
+    telephonyManager.setDataEnabledForReason(correctReason, true);
+    assertThat(telephonyManager.isDataEnabledForReason(correctReason)).isTrue();
+    assertThat(telephonyManager.isDataEnabledForReason(incorrectReason)).isTrue();
+  }
+
+  @Test
   public void setDataStateChangesDataState() {
     assertThat(telephonyManager.getDataState()).isEqualTo(TelephonyManager.DATA_DISCONNECTED);
     shadowOf(telephonyManager).setDataState(TelephonyManager.DATA_CONNECTING);
@@ -1068,4 +1105,36 @@
   public void getEmergencyCallback_notSet_returnsFalse() {
     assertThat(telephonyManager.getEmergencyCallbackMode()).isFalse();
   }
+
+  @Test
+  @Config(minSdk = R)
+  public void getEmergencyNumbersList_notSet_returnsEmptyList() {
+    assertThat(telephonyManager.getEmergencyNumberList()).isEmpty();
+  }
+
+  @Test
+  @Config(minSdk = R)
+  public void getEmergencyNumbersList_wasSet_returnsCorrectList() throws Exception {
+    EmergencyNumber emergencyNumber =
+        EmergencyNumber.class
+            .getConstructor(
+                String.class,
+                String.class,
+                String.class,
+                int.class,
+                List.class,
+                int.class,
+                int.class)
+            .newInstance(
+                "911",
+                "us",
+                "30",
+                EMERGENCY_NUMBER_SOURCE_DATABASE,
+                ImmutableList.of(),
+                EMERGENCY_SERVICE_CATEGORY_POLICE,
+                EmergencyNumber.EMERGENCY_CALL_ROUTING_NORMAL);
+    ShadowTelephonyManager.setEmergencyNumberList(
+        ImmutableMap.of(0, ImmutableList.of(emergencyNumber)));
+    assertThat(telephonyManager.getEmergencyNumberList().get(0)).containsExactly(emergencyNumber);
+  }
 }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
index 75df110..7c9bfaf 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowUserManagerTest.java
@@ -9,6 +9,7 @@
 import static android.os.Build.VERSION_CODES.N_MR1;
 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 static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
 import static org.robolectric.Shadows.shadowOf;
@@ -68,7 +69,8 @@
     UserHandle anotherProfile = newUserHandle(2);
     shadowOf(userManager).addUserProfile(anotherProfile);
 
-    assertThat(userManager.getUserProfiles()).containsExactly(Process.myUserHandle(), anotherProfile);
+    assertThat(userManager.getUserProfiles())
+        .containsExactly(Process.myUserHandle(), anotherProfile);
   }
 
   @Test
@@ -243,7 +245,8 @@
     try {
       userManager.isManagedProfile();
       fail("Expected exception");
-    } catch (SecurityException expected) {}
+    } catch (SecurityException expected) {
+    }
 
     setPermissions(permission.MANAGE_USERS);
 
@@ -317,6 +320,19 @@
   }
 
   @Test
+  @Config(minSdk = R)
+  public void getUserHandles() {
+    assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(1);
+    shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(0);
+    assertThat(UserHandle.myUserId()).isEqualTo(UserHandle.USER_SYSTEM);
+
+    UserHandle expectedUserHandle = shadowOf(userManager).addUser(10, "secondary_user", 0);
+    assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).size()).isEqualTo(2);
+    assertThat(shadowOf(userManager).getUserHandles(/* excludeDying= */ true).get(1))
+        .isEqualTo(expectedUserHandle);
+  }
+
+  @Test
   @Config(minSdk = N_MR1, maxSdk = Q)
   public void isDemoUser() {
     // All methods are based on the current user, so no need to pass a UserHandle.
@@ -565,6 +581,34 @@
   }
 
   @Test
+  @Config(minSdk = Q)
+  public void removeSecondaryUser_noExistingUser_doesNotRemove() {
+    assertThat(shadowOf(userManager).removeUser(UserHandle.of(10))).isFalse();
+    assertThat(userManager.getUserCount()).isEqualTo(1);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void removeUserWhenPossible_twoUsersRemoveOne_hasOneUserLeft() {
+    shadowOf(userManager).addUser(10, "secondary_user", 0);
+    assertThat(
+            userManager.removeUserWhenPossible(
+                UserHandle.of(10), /* overrideDevicePolicy= */ false))
+        .isEqualTo(UserManager.REMOVE_RESULT_REMOVED);
+    assertThat(userManager.getUserCount()).isEqualTo(1);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void removeUserWhenPossible_nonExistingUser_fails() {
+    assertThat(
+            userManager.removeUserWhenPossible(
+                UserHandle.of(10), /* overrideDevicePolicy= */ false))
+        .isEqualTo(UserManager.REMOVE_RESULT_ERROR_UNKNOWN);
+    assertThat(userManager.getUserCount()).isEqualTo(1);
+  }
+
+  @Test
   @Config(minSdk = JELLY_BEAN_MR1)
   public void switchToSecondaryUser() {
     shadowOf(userManager).addUser(10, "secondary_user", 0);
@@ -653,8 +697,8 @@
   @Config(minSdk = LOLLIPOP)
   public void getProfiles_addedProfile_containsProfile() {
     shadowOf(userManager).addUser(TEST_USER_HANDLE, "", 0);
-    shadowOf(userManager).addProfile(
-        TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
+    shadowOf(userManager)
+        .addProfile(TEST_USER_HANDLE, PROFILE_USER_HANDLE, PROFILE_USER_NAME, PROFILE_USER_FLAGS);
 
     // getProfiles(userId) include user itself and asssociated profiles.
     assertThat(userManager.getProfiles(TEST_USER_HANDLE).get(0).id).isEqualTo(TEST_USER_HANDLE);
@@ -850,7 +894,6 @@
     assertThat(UserManager.supportsMultipleUsers()).isTrue();
   }
 
-
   @Test
   @Config(minSdk = Q)
   public void getUserSwitchability_shouldReturnLastSetSwitchability() {
@@ -859,8 +902,7 @@
         .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
     assertThat(userManager.getUserSwitchability())
         .isEqualTo(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
-    shadowOf(userManager)
-        .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+    shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
     assertThat(userManager.getUserSwitchability()).isEqualTo(UserManager.SWITCHABILITY_STATUS_OK);
   }
 
@@ -880,8 +922,7 @@
     shadowOf(userManager)
         .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
     assertThat(userManager.canSwitchUsers()).isFalse();
-    shadowOf(userManager)
-        .setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
+    shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
     assertThat(userManager.canSwitchUsers()).isTrue();
   }
 
@@ -889,7 +930,7 @@
   @Config(minSdk = Q)
   public void getUserName_shouldReturnSetUserName() {
     shadowOf(userManager).setUserSwitchability(UserManager.SWITCHABILITY_STATUS_OK);
-    shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+    shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
     shadowOf(userManager).switchUser(10);
     assertThat(userManager.getUserName()).isEqualTo(PROFILE_USER_NAME);
   }
@@ -900,7 +941,7 @@
     userManager.setUserIcon(TEST_USER_ICON);
     assertThat(userManager.getUserIcon()).isEqualTo(TEST_USER_ICON);
 
-    shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags = */ 0);
+    shadowOf(userManager).addUser(10, PROFILE_USER_NAME, /* flags= */ 0);
     shadowOf(userManager).switchUser(10);
     assertThat(userManager.getUserIcon()).isNull();
   }
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
index b8b5785..ef5527a 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVibratorTest.java
@@ -16,7 +16,6 @@
 import android.media.AudioAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
-import android.os.vibrator.PrimitiveSegment;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import com.google.common.collect.ImmutableList;
@@ -132,7 +131,7 @@
 
   @Config(minSdk = S)
   @Test
-  public void getVibrationEffectSegments_composeOnce_shouldReturnSameFragment() {
+  public void getPrimitiveSegmentsInPrimitiveEffects_composeOnce_shouldReturnSameFragment() {
     vibrator.vibrate(
         VibrationEffect.startComposition()
             .addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20)
@@ -140,17 +139,17 @@
             .addPrimitive(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)
             .compose());
 
-    assertThat(shadowOf(vibrator).getVibrationEffectSegments())
+    assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects())
         .isEqualTo(
             ImmutableList.of(
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20),
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50),
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)));
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20),
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.7f, /* delay= */ 50),
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150)));
   }
 
   @Config(minSdk = S)
   @Test
-  public void getVibrationEffectSegments_composeTwice_shouldReturnTheLastComposition() {
+  public void getPrimitiveSegmentsInPrimitiveEffects_composeTwice_shouldReturnTheLastComposition() {
     vibrator.vibrate(
         VibrationEffect.startComposition()
             .addPrimitive(EFFECT_CLICK, /* scale= */ 0.5f, /* delay= */ 20)
@@ -164,12 +163,12 @@
             .addPrimitive(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)
             .compose());
 
-    assertThat(shadowOf(vibrator).getVibrationEffectSegments())
+    assertThat(shadowOf(vibrator).getPrimitiveSegmentsInPrimitiveEffects())
         .isEqualTo(
             ImmutableList.of(
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120),
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150),
-                new PrimitiveSegment(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)));
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.4f, /* delay= */ 120),
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 0.9f, /* delay= */ 150),
+                new PrimitiveEffect(EFFECT_CLICK, /* scale= */ 1f, /* delay= */ 2150)));
   }
 
   @Config(minSdk = R)
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
new file mode 100644
index 0000000..dbf7250
--- /dev/null
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowVpnManagerTest.java
@@ -0,0 +1,95 @@
+package org.robolectric.shadows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.net.Ikev2VpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+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.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(minSdk = VERSION_CODES.R)
+public class ShadowVpnManagerTest {
+  private VpnManager vpnManager;
+  private ShadowVpnManager shadowVpnManager;
+
+  @Before
+  public void setUp() throws Exception {
+    vpnManager = ApplicationProvider.getApplicationContext().getSystemService(VpnManager.class);
+    shadowVpnManager = shadowOf(vpnManager);
+  }
+
+  @Test
+  public void provisionVpnProfile() {
+    Intent intent = new Intent("foo");
+    shadowVpnManager.setProvisionVpnProfileResult(intent);
+
+    assertThat(
+            vpnManager.provisionVpnProfile(
+                new Ikev2VpnProfile.Builder("server", "local.identity")
+                    .setAuthPsk(new byte[0])
+                    .build()))
+        .isSameInstanceAs(intent);
+
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+      VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+      assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+      assertThat(state.getSessionId()).isNull();
+    }
+  }
+
+  @Test
+  public void deleteVpnProfile() {
+    vpnManager.provisionVpnProfile(
+        new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+    vpnManager.deleteProvisionedVpnProfile();
+  }
+
+  @Test
+  @Config(minSdk = VERSION_CODES.TIRAMISU)
+  public void deleteVpnProfile_tiramisu() {
+    vpnManager.provisionVpnProfile(
+        new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+    assertThat(vpnManager.getProvisionedVpnProfileState()).isNotNull();
+
+    vpnManager.deleteProvisionedVpnProfile();
+    assertThat(vpnManager.getProvisionedVpnProfileState()).isNull();
+  }
+
+  @Test
+  public void startAndStopVpnProfile() {
+    vpnManager.provisionVpnProfile(
+        new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+    vpnManager.startProvisionedVpnProfile();
+    vpnManager.stopProvisionedVpnProfile();
+  }
+
+  @Test
+  @Config(minSdk = VERSION_CODES.TIRAMISU)
+  public void startAndStopVpnProfile_tiramisu() {
+    vpnManager.provisionVpnProfile(
+        new Ikev2VpnProfile.Builder("server", "local.identity").setAuthPsk(new byte[0]).build());
+    String sessionKey = vpnManager.startProvisionedVpnProfileSession();
+    VpnProfileState state = vpnManager.getProvisionedVpnProfileState();
+    assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_CONNECTED);
+    assertThat(state.getSessionId()).isEqualTo(sessionKey);
+    assertThat(state.isAlwaysOn()).isFalse();
+    assertThat(state.isLockdownEnabled()).isFalse();
+
+    vpnManager.stopProvisionedVpnProfile();
+    state = vpnManager.getProvisionedVpnProfileState();
+    assertThat(state.getState()).isEqualTo(VpnProfileState.STATE_DISCONNECTED);
+    assertThat(state.getSessionId()).isNull();
+    assertThat(state.isAlwaysOn()).isFalse();
+    assertThat(state.isLockdownEnabled()).isFalse();
+  }
+}
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
index 7a1bee6..15dd9ec 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowWifiManagerTest.java
@@ -1,12 +1,17 @@
 package org.robolectric.shadows;
 
+import static android.net.wifi.WifiManager.SCAN_RESULTS_AVAILABLE_ACTION;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 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 androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -14,10 +19,12 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.robolectric.Shadows.shadowOf;
 
+import android.app.Application;
 import android.app.admin.DeviceAdminService;
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.net.ConnectivityManager;
 import android.net.DhcpInfo;
 import android.net.NetworkInfo;
@@ -27,13 +34,17 @@
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
+import android.net.wifi.WifiManager.PnoScanResultsCallback;
+import android.net.wifi.WifiSsid;
 import android.net.wifi.WifiUsabilityStatsEntry;
 import android.os.Build;
 import android.util.Pair;
-import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -47,9 +58,7 @@
 
   @Before
   public void setUp() throws Exception {
-    wifiManager =
-        (WifiManager)
-            ApplicationProvider.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+    wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
   }
 
   @Test
@@ -494,8 +503,7 @@
     // THEN
     NetworkInfo networkInfo =
         ((ConnectivityManager)
-                ApplicationProvider.getApplicationContext()
-                    .getSystemService(Context.CONNECTIVITY_SERVICE))
+                getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE))
             .getActiveNetworkInfo();
     assertThat(networkInfo.getType()).isEqualTo(ConnectivityManager.TYPE_WIFI);
     assertThat(networkInfo.isConnected()).isTrue();
@@ -784,13 +792,305 @@
     assertThat(shadowOf(wifiManager).getSoftApConfiguration().getSsid()).isEqualTo("foo");
   }
 
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_nullCallback_throwsIllegalArgumentException() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+                /* frequencies= */ null,
+                Executors.newSingleThreadExecutor(),
+                /* callback= */ null));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_nullExecutor_throwsIllegalArgumentException() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                List.of(WifiSsid.fromBytes(new byte[] {3, 2, 5})),
+                /* frequencies= */ null,
+                /* executor= */ null,
+                new TestPnoScanResultsCallback()));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_nullSsidList_throwsIllegalStateException() {
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                /* ssids= */ null,
+                /* frequencies= */ null,
+                Executors.newSingleThreadExecutor(),
+                new TestPnoScanResultsCallback()));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_emptySsidList_throwsIllegalStateException() {
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                /* ssids= */ List.of(),
+                /* frequencies= */ null,
+                Executors.newSingleThreadExecutor(),
+                new TestPnoScanResultsCallback()));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_moreThan2Ssids_throwsIllegalArgumentException() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                List.of(
+                    WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+                    WifiSsid.fromBytes(new byte[] {9, 8, 7, 6}),
+                    WifiSsid.fromBytes(new byte[] {90, 81, 72, 63, 54})),
+                /* frequencies= */ null,
+                Executors.newSingleThreadExecutor(),
+                new TestPnoScanResultsCallback()));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_moreThan10Frequencies_throwsIllegalArgumentException() {
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            wifiManager.setExternalPnoScanRequest(
+                List.of(
+                    WifiSsid.fromBytes(new byte[] {1, 2, 3}),
+                    WifiSsid.fromBytes(new byte[] {9, 8, 7, 6})),
+                new int[] {5160, 5180, 5200, 5220, 5240, 5260, 5280, 5300, 5320, 5340, 5360},
+                Executors.newSingleThreadExecutor(),
+                new TestPnoScanResultsCallback()));
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_validRequest_successCallbackInvoked() throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        Executors.newSingleThreadExecutor(),
+        callback);
+
+    assertThat(callback.successfulRegistrations.take()).isNotNull();
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void
+      setExternalPnoScanRequest_outstandingRequest_failureCallbackInvokedWithAlreadyRegisteredStatus()
+          throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        Executors.newSingleThreadExecutor(),
+        callback);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {9, 2, 5})),
+        new int[] {5280},
+        Executors.newSingleThreadExecutor(),
+        callback);
+
+    assertThat(callback.failedRegistrations.take())
+        .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void setExternalPnoScanRequest_differentUid_failureCallbackInvokedWithBusyStatus()
+      throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        Executors.newSingleThreadExecutor(),
+        callback);
+
+    int firstAppUid = ShadowProcess.myUid();
+    int secondAppUid;
+    do {
+      secondAppUid = ShadowProcess.getRandomApplicationUid();
+    } while (firstAppUid == secondAppUid);
+    ShadowProcess.setUid(secondAppUid);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        Executors.newSingleThreadExecutor(),
+        callback);
+
+    assertThat(callback.failedRegistrations.take())
+        .isEqualTo(PnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void clearExternalPnoScanRequest_outstandingRequest_callbackInvokedWithUnregisteredStatus()
+      throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        Executors.newSingleThreadExecutor(),
+        callback);
+    wifiManager.clearExternalPnoScanRequest();
+
+    assertThat(callback.removedRegistrations.take())
+        .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void clearExternalPnoScanRequest_wrongUid_callbackNotInvoked() throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(WifiSsid.fromBytes(new byte[] {1, 2, 3})),
+        /* frequencies= */ null,
+        executor,
+        callback);
+
+    int firstAppUid = ShadowProcess.myUid();
+    int secondAppUid;
+    do {
+      secondAppUid = ShadowProcess.getRandomApplicationUid();
+    } while (firstAppUid == secondAppUid);
+    ShadowProcess.setUid(secondAppUid);
+
+    wifiManager.clearExternalPnoScanRequest();
+
+    executor.shutdown();
+
+    assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+    assertThat(callback.removedRegistrations).isEmpty();
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void networksFoundFromPnoScan_matchingSsid_availableCallbackInvoked() throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+    WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+    ScanResult scanResult = new ScanResult();
+    scanResult.setWifiSsid(wifiSsid);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+    shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+    assertThat(callback.incomingScanResults.take()).containsExactly(scanResult);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void networksFoundFromPnoScan_matchingSsid_removedCallbackInvokedWithDeliveredStatus()
+      throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+    WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+    ScanResult scanResult = new ScanResult();
+    scanResult.setWifiSsid(wifiSsid);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+    shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+    assertThat(callback.removedRegistrations.take())
+        .isEqualTo(PnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED);
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void networksFoundFromPnoScan_matchingSsid_scanResultsAvailableBroadcastSent() {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+    WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+    ScanResult scanResult = new ScanResult();
+    scanResult.setWifiSsid(wifiSsid);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(wifiSsid), /* frequencies= */ null, Executors.newSingleThreadExecutor(), callback);
+    shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+    Intent expectedIntent = new Intent(SCAN_RESULTS_AVAILABLE_ACTION);
+    expectedIntent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+    expectedIntent.setPackage(getApplicationContext().getPackageName());
+
+    assertThat(
+            shadowOf((Application) getApplicationContext()).getBroadcastIntents().stream()
+                .anyMatch(expectedIntent::filterEquals))
+        .isTrue();
+  }
+
+  @Test
+  @Config(minSdk = TIRAMISU)
+  public void networksFoundFromPnoScan_noMatchingSsid_availableCallbackNotInvoked()
+      throws Exception {
+    TestPnoScanResultsCallback callback = new TestPnoScanResultsCallback();
+    ExecutorService executor = Executors.newSingleThreadExecutor();
+    WifiSsid wifiSsid = WifiSsid.fromBytes(new byte[] {1, 2, 3});
+    WifiSsid otherWifiSsid = WifiSsid.fromBytes(new byte[] {9, 8, 7, 6});
+    ScanResult scanResult = new ScanResult();
+    scanResult.setWifiSsid(otherWifiSsid);
+
+    wifiManager.setExternalPnoScanRequest(
+        List.of(wifiSsid), /* frequencies= */ null, executor, callback);
+    shadowOf(wifiManager).networksFoundFromPnoScan(List.of(scanResult));
+
+    executor.shutdown();
+
+    assertThat(executor.awaitTermination(5, MINUTES)).isTrue();
+    assertThat(callback.incomingScanResults).isEmpty();
+  }
+
+  private class TestPnoScanResultsCallback implements PnoScanResultsCallback {
+    LinkedBlockingQueue<List<ScanResult>> incomingScanResults = new LinkedBlockingQueue<>();
+    LinkedBlockingQueue<Object> successfulRegistrations = new LinkedBlockingQueue<>();
+    LinkedBlockingQueue<Integer> failedRegistrations = new LinkedBlockingQueue<>();
+    LinkedBlockingQueue<Integer> removedRegistrations = new LinkedBlockingQueue<>();
+
+    @Override
+    public void onScanResultsAvailable(List<ScanResult> scanResults) {
+      incomingScanResults.add(scanResults);
+    }
+
+    @Override
+    public void onRegisterSuccess() {
+      successfulRegistrations.add(new Object());
+    }
+
+    @Override
+    public void onRegisterFailed(int reason) {
+      failedRegistrations.add(reason);
+    }
+
+    @Override
+    public void onRemoved(int reason) {
+      removedRegistrations.add(reason);
+    }
+  }
+
   private void setDeviceOwner() {
     shadowOf(
             (DevicePolicyManager)
-                ApplicationProvider.getApplicationContext()
-                    .getSystemService(Context.DEVICE_POLICY_SERVICE))
-        .setDeviceOwner(
-            new ComponentName(
-                ApplicationProvider.getApplicationContext(), DeviceAdminService.class));
+                getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE))
+        .setDeviceOwner(new ComponentName(getApplicationContext(), DeviceAdminService.class));
   }
 }
diff --git a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
index 3b06a4e..612fd3b 100644
--- a/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
+++ b/robolectric/src/test/java/org/robolectric/util/SQLiteLibraryLoaderTest.java
@@ -1,8 +1,8 @@
 package org.robolectric.util;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeTrue;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import org.junit.After;
@@ -56,7 +56,7 @@
 
   @Test
   public void shouldExtractNativeLibrary() {
-    assumeTrue(SQLiteLibraryLoader.isOsSupported());
+    assume().that(SQLiteLibraryLoader.isOsSupported()).isTrue();
     assertThat(loader.isLoaded()).isFalse();
     loader.doLoad();
     assertThat(loader.isLoaded()).isTrue();
diff --git a/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml
new file mode 100644
index 0000000..cbda17e
--- /dev/null
+++ b/robolectric/src/test/resources/TestAndroidManifestWithAppComponentFactory.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.robolectric">
+  <uses-sdk android:targetSdkVersion="18"/>
+
+  <application
+      android:appComponentFactory="org.robolectric.CustomAppComponentFactory">
+    <receiver
+        android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithOneActionReceiver">
+      <intent-filter>
+        <action android:name="org.robolectric.ACTION_CUSTOM_CONSTRUCTOR"/>
+      </intent-filter>
+    </receiver>
+    <receiver
+      android:name=".CustomConstructorReceiverWrapper$CustomConstructorWithEmptyActionReceiver" />
+  </application>
+</manifest>
diff --git a/sandbox/build.gradle b/sandbox/build.gradle
index 64accd7..358b027 100644
--- a/sandbox/build.gradle
+++ b/sandbox/build.gradle
@@ -5,24 +5,24 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    annotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
-    annotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
+    annotationProcessor libs.auto.service
+    annotationProcessor libs.error.prone.core
 
     api project(":annotations")
     api project(":utils")
     api project(":shadowapi")
     api project(":utils:reflector")
-    compileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
-    api "javax.annotation:javax.annotation-api:1.3.2"
-    api "javax.inject:javax.inject:1"
+    compileOnly libs.auto.service.annotations
+    api libs.javax.annotation.api
+    api libs.javax.inject
 
-    api "org.ow2.asm:asm:${asmVersion}"
-    api "org.ow2.asm:asm-commons:${asmVersion}"
-    api "com.google.guava:guava:$guavaJREVersion"
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    api libs.asm
+    api libs.asm.commons
+    api libs.guava
+    compileOnly libs.findbugs.jsr305
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
     testImplementation project(":junit")
 }
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 fac0022..e1463a1 100644
--- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
+++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java
@@ -10,6 +10,7 @@
 import java.lang.reflect.Modifier;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Objects;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.ConstantDynamic;
@@ -212,23 +213,25 @@
   }
 
   /**
-   * Checks if the first instruction is a Jacoco load instructions. Robolectric is not capable at
-   * the moment of re-instrumenting Jacoco-instrumented constructors.
+   * Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not
+   * capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are
+   * currently skipped.
    *
    * @param ctor constructor method node
    * @return whether or not the constructor can be instrumented
    */
   private boolean isJacocoInstrumented(MethodNode ctor) {
     AbstractInsnNode[] insns = ctor.instructions.toArray();
-    if (insns.length > 0) {
-      if (insns[0] instanceof LdcInsnNode
-          && ((LdcInsnNode) insns[0]).cst instanceof ConstantDynamic) {
-        ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) insns[0]).cst;
+    if (insns.length > 1) {
+      AbstractInsnNode node = insns[0];
+      if (node instanceof LabelNode) {
+        node = insns[1];
+      }
+      if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) {
+        ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst;
         return cst.getName().equals("$jacocoData");
-      } else if (insns.length > 1
-          && insns[0] instanceof LabelNode
-          && insns[1] instanceof MethodInsnNode) {
-        return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name);
+      } else if (node instanceof MethodInsnNode) {
+        return Objects.equals(((MethodInsnNode) node).name, "$jacocoInit");
       }
     }
     return false;
diff --git a/settings.gradle b/settings.gradle
index 20f8fae..1894b8c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -16,6 +16,7 @@
 include ":shadows:httpclient"
 include ":shadows:multidex"
 include ":shadows:playservices"
+include ":shadows:versioning"
 include ":shadowapi"
 include ":errorprone"
 include ":nativeruntime"
diff --git a/shadowapi/build.gradle b/shadowapi/build.gradle
index f63d048..3f0064f 100644
--- a/shadowapi/build.gradle
+++ b/shadowapi/build.gradle
@@ -5,11 +5,11 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    compileOnly libs.findbugs.jsr305
 
     api project(":annotations")
     api project(":utils")
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
-}
\ No newline at end of file
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
+}
diff --git a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
index eaaee1a..8ae6399 100644
--- a/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
+++ b/shadowapi/src/main/java/org/robolectric/util/ReflectionHelpers.java
@@ -10,7 +10,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import javax.annotation.Nullable;
 
 /** Collection of helper methods for calling methods and accessing fields reflectively. */
 @SuppressWarnings(value = {"unchecked", "TypeParameterUnusedInFormals", "NewApi"})
@@ -45,9 +44,10 @@
    * <p>The returned object will be an instance of the given class, but all methods will return
    * either the "default" value for primitives, or another deep proxy for non-primitive types.
    *
-   * <p>This should be used rarely, for cases where we need to create deep proxies in order not
-   * to crash. The inner proxies are impossible to configure, so there is no way to create
-   * meaningful behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions.
+   * <p>This should be used rarely, for cases where we need to create deep proxies in order not to
+   * crash. The inner proxies are impossible to configure, so there is no way to create meaningful
+   * behavior from a deep proxy. It serves mainly to prevent Null Pointer Exceptions.
+   *
    * @param clazz the class to provide a proxy instance of.
    * @return a new "Deep Proxy" instance of the given class.
    */
@@ -127,7 +127,8 @@
    * @param fieldName The field name.
    * @param fieldNewValue New value.
    */
-  public static void setField(final Object object, final String fieldName, final Object fieldNewValue) {
+  public static void setField(
+      final Object object, final String fieldName, final Object fieldNewValue) {
     try {
       traverseClassHierarchy(
           object.getClass(),
@@ -152,7 +153,8 @@
    * @param fieldName The field name.
    * @param fieldNewValue New value.
    */
-  public static void setField(Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) {
+  public static void setField(
+      Class<?> type, final Object object, final String fieldName, final Object fieldNewValue) {
     try {
       Field field = type.getDeclaredField(fieldName);
       field.setAccessible(true);
@@ -163,6 +165,22 @@
   }
 
   /**
+   * Reflectively check if a class has a given field (static or non static).
+   *
+   * @param clazz Target class.
+   * @param fieldName The field name.
+   * @return boolean to indicate whether the field exists or not in clazz.
+   */
+  public static boolean hasField(Class<?> clazz, String fieldName) {
+    try {
+      Field field = clazz.getDeclaredField(fieldName);
+      return (field != null);
+    } catch (NoSuchFieldException e) {
+      return false;
+    }
+  }
+
+  /**
    * Reflectively get the value of a static field.
    *
    * @param field Field object.
@@ -392,7 +410,9 @@
   public static <T> T newInstance(Class<T> cl) {
     try {
       return cl.getDeclaredConstructor().newInstance();
-    } catch (InstantiationException | IllegalAccessException | NoSuchMethodException
+    } catch (InstantiationException
+        | IllegalAccessException
+        | NoSuchMethodException
         | InvocationTargetException e) {
       throw new RuntimeException(e);
     }
@@ -465,15 +485,15 @@
    */
   public static class ClassParameter<V> {
     public final Class<? extends V> clazz;
-    public final V val;
+    public final V value;
 
-    public ClassParameter(Class<? extends V> clazz, V val) {
+    public ClassParameter(Class<? extends V> clazz, V value) {
       this.clazz = clazz;
-      this.val = val;
+      this.value = value;
     }
 
-    public static <V> ClassParameter<V> from(Class<? extends V> clazz, V val) {
-      return new ClassParameter<>(clazz, val);
+    public static <V> ClassParameter<V> from(Class<? extends V> clazz, V value) {
+      return new ClassParameter<>(clazz, value);
     }
 
     public static ClassParameter<?>[] fromComponentLists(Class<?>[] classes, Object[] values) {
@@ -496,7 +516,7 @@
     public static Object[] getValues(ClassParameter<?>... classParameters) {
       Object[] values = new Object[classParameters.length];
       for (int i = 0; i < classParameters.length; i++) {
-        Object paramValue = classParameters[i].val;
+        Object paramValue = classParameters[i].value;
         values[i] = paramValue;
       }
       return values;
@@ -510,15 +530,15 @@
    */
   public static class StringParameter<V> {
     public final String className;
-    public final V val;
+    public final V value;
 
-    public StringParameter(String className, V val) {
+    public StringParameter(String className, V value) {
       this.className = className;
-      this.val = val;
+      this.value = value;
     }
 
-    public static <V> StringParameter<V> from(String className, V val) {
-      return new StringParameter<>(className, val);
+    public static <V> StringParameter<V> from(String className, V value) {
+      return new StringParameter<>(className, value);
     }
   }
 }
diff --git a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
index 56c489d..e5f281b 100644
--- a/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
+++ b/shadowapi/src/test/java/org/robolectric/util/ReflectionHelpersTest.java
@@ -141,7 +141,8 @@
   }
 
   @Test
-  public void callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
+  public void
+      callInstanceMethodReflectively_whenMultipleSignaturesExistForAMethodName_callsMethodWithCorrectSignature() {
     ExampleDescendant example = new ExampleDescendant();
     int returnNumber =
         ReflectionHelpers.callInstanceMethod(
@@ -282,23 +283,35 @@
   }
 
   @Test
-  public void callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() {
-    ExampleClass ec = ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16));
+  public void
+      callConstructorReflectively_whenMultipleSignaturesExistForTheConstructor_callsConstructorWithCorrectSignature() {
+    ExampleClass ec =
+        ReflectionHelpers.callConstructor(ExampleClass.class, ClassParameter.from(int.class, 16));
     assertWithMessage("index").that(ec.index).isEqualTo(16);
     assertWithMessage("name").that(ec.name).isNull();
   }
 
-  @SuppressWarnings("serial")
-  private static class TestError extends Error {
+  @Test
+  public void callHasField_withstaticandregularmember() {
+    assertWithMessage("has field failed for member: unusedName")
+        .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedName"))
+        .isTrue();
+    assertWithMessage("has field failed for member: unusedStaticName")
+        .that(ReflectionHelpers.hasField(FieldTestClass.class, "unusedStaticName"))
+        .isTrue();
+    assertWithMessage("has field failed for non existant member: noname")
+        .that(ReflectionHelpers.hasField(FieldTestClass.class, "noname"))
+        .isFalse();
   }
 
   @SuppressWarnings("serial")
-  private static class TestException extends Exception {
-  }
+  private static class TestError extends Error {}
 
   @SuppressWarnings("serial")
-  private static class TestRuntimeException extends RuntimeException {
-  }
+  private static class TestException extends Exception {}
+
+  @SuppressWarnings("serial")
+  private static class TestRuntimeException extends RuntimeException {}
 
   @SuppressWarnings("unused")
   private static class ExampleBase {
@@ -406,4 +419,11 @@
       this.index = index;
     }
   }
+
+  private static class FieldTestClass {
+    public String unusedName;
+    public static String unusedStaticName = "unusedStaticNameValue";
+
+    private FieldTestClass() {}
+  }
 }
diff --git a/shadows/framework/build.gradle b/shadows/framework/build.gradle
index a273d5a..a2230b0 100644
--- a/shadows/framework/build.gradle
+++ b/shadows/framework/build.gradle
@@ -15,6 +15,8 @@
     sqlite4java
 }
 
+def sqlite4javaVersion = libs.versions.sqlite4java.get()
+
 task copySqliteNatives(type: Copy) {
     from project.configurations.sqlite4java {
         include '**/*.dll'
@@ -45,22 +47,20 @@
     api project(":pluginapi")
     api project(":sandbox")
     api project(":shadowapi")
+    api project(":shadows:versioning")
     api project(":utils")
     api project(":utils:reflector")
+
     api "androidx.test:monitor:$axtMonitorVersion@aar"
 
-    implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion"
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
-    api "com.almworks.sqlite4java:sqlite4java:$sqlite4javaVersion"
-    compileOnly(AndroidSdk.MAX_SDK.coordinates) { force = true }
-    api "com.ibm.icu:icu4j:72.1"
-    api "androidx.annotation:annotation:1.1.0"
-    api "com.google.auto.value:auto-value-annotations:1.10.1"
-    annotationProcessor "com.google.auto.value:auto-value:1.10.1"
+    implementation libs.error.prone.annotations
+    compileOnly libs.findbugs.jsr305
+    api libs.sqlite4java
+    compileOnly(AndroidSdk.MAX_SDK.coordinates)
+    api libs.icu4j
+    api libs.androidx.annotation
+    api libs.auto.value.annotations
+    annotationProcessor libs.auto.value
 
-    sqlite4java "com.almworks.sqlite4java:libsqlite4java-osx:$sqlite4javaVersion"
-    sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-amd64:$sqlite4javaVersion"
-    sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x64:$sqlite4javaVersion"
-    sqlite4java "com.almworks.sqlite4java:libsqlite4java-linux-i386:$sqlite4javaVersion"
-    sqlite4java "com.almworks.sqlite4java:sqlite4java-win32-x86:$sqlite4javaVersion"
+    sqlite4java libs.bundles.sqlite4java.native
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java b/shadows/framework/src/main/java/org/robolectric/RuntimeEnvironment.java
old mode 100755
new mode 100644
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
new file mode 100644
index 0000000..e2b8f0d
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/AssociationInfoBuilder.java
@@ -0,0 +1,138 @@
+package org.robolectric.shadows;
+
+import static android.os.Build.VERSION_CODES.TIRAMISU;
+
+import android.companion.AssociationInfo;
+import android.net.MacAddress;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.ReflectionHelpers.ClassParameter;
+
+/** Builder for {@link AssociationInfo}. */
+public class AssociationInfoBuilder {
+  private int id;
+  private int userId;
+  private String packageName;
+  private String deviceMacAddress;
+  private CharSequence displayName;
+  private String deviceProfile;
+  private boolean selfManaged;
+  private boolean notifyOnDeviceNearby;
+  private long approvedMs;
+  private long lastTimeConnectedMs;
+
+  private AssociationInfoBuilder() {}
+
+  public static AssociationInfoBuilder newBuilder() {
+    return new AssociationInfoBuilder();
+  }
+
+  public AssociationInfoBuilder setId(int id) {
+    this.id = id;
+    return this;
+  }
+
+  public AssociationInfoBuilder setUserId(int userId) {
+    this.userId = userId;
+    return this;
+  }
+
+  public AssociationInfoBuilder setPackageName(String packageName) {
+    this.packageName = packageName;
+    return this;
+  }
+
+  public AssociationInfoBuilder setDeviceMacAddress(String deviceMacAddress) {
+    this.deviceMacAddress = deviceMacAddress;
+    return this;
+  }
+
+  public AssociationInfoBuilder setDisplayName(CharSequence displayName) {
+    this.displayName = displayName;
+    return this;
+  }
+
+  public AssociationInfoBuilder setDeviceProfile(String deviceProfile) {
+    this.deviceProfile = deviceProfile;
+    return this;
+  }
+
+  public AssociationInfoBuilder setSelfManaged(boolean selfManaged) {
+    this.selfManaged = selfManaged;
+    return this;
+  }
+
+  public AssociationInfoBuilder setNotifyOnDeviceNearby(boolean notifyOnDeviceNearby) {
+    this.notifyOnDeviceNearby = notifyOnDeviceNearby;
+    return this;
+  }
+
+  public AssociationInfoBuilder setApprovedMs(long approvedMs) {
+    this.approvedMs = approvedMs;
+    return this;
+  }
+
+  public AssociationInfoBuilder setLastTimeConnectedMs(long lastTimeConnectedMs) {
+    this.lastTimeConnectedMs = lastTimeConnectedMs;
+    return this;
+  }
+
+  public AssociationInfo build() {
+    try {
+      if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+        // We have two different constructors for AssociationInfo across
+        // T branches. aosp has the constructor that takes a new "revoked" parameter.
+        // Since there is not deterministic way to know which branch we are running in,
+        // we will reflect on the class to see if it has the mRevoked member.
+        // Based on the result we will either invoke the constructor with "revoked" or the
+        // one without this parameter.
+        if (ReflectionHelpers.hasField(AssociationInfo.class, "mRevoked")) {
+          return ReflectionHelpers.callConstructor(
+              AssociationInfo.class,
+              ClassParameter.from(int.class, id),
+              ClassParameter.from(int.class, userId),
+              ClassParameter.from(String.class, packageName),
+              ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+              ClassParameter.from(CharSequence.class, displayName),
+              ClassParameter.from(String.class, deviceProfile),
+              ClassParameter.from(boolean.class, selfManaged),
+              ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+              ClassParameter.from(boolean.class, false /*revoked only supported in aosp*/),
+              ClassParameter.from(long.class, approvedMs),
+              ClassParameter.from(long.class, lastTimeConnectedMs));
+        } else {
+          return ReflectionHelpers.callConstructor(
+              AssociationInfo.class,
+              ClassParameter.from(int.class, id),
+              ClassParameter.from(int.class, userId),
+              ClassParameter.from(String.class, packageName),
+              ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+              ClassParameter.from(CharSequence.class, displayName),
+              ClassParameter.from(String.class, deviceProfile),
+              ClassParameter.from(boolean.class, selfManaged),
+              ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+              ClassParameter.from(long.class, approvedMs),
+              ClassParameter.from(long.class, lastTimeConnectedMs));
+        }
+      } else {
+        return ReflectionHelpers.callConstructor(
+            AssociationInfo.class,
+            ClassParameter.from(int.class, id),
+            ClassParameter.from(int.class, userId),
+            ClassParameter.from(String.class, packageName),
+            ClassParameter.from(MacAddress.class, MacAddress.fromString(deviceMacAddress)),
+            ClassParameter.from(CharSequence.class, displayName),
+            ClassParameter.from(String.class, deviceProfile),
+            ClassParameter.from(Class.forName("android.companion.AssociatedDevice"), null),
+            ClassParameter.from(boolean.class, selfManaged),
+            ClassParameter.from(boolean.class, notifyOnDeviceNearby),
+            ClassParameter.from(boolean.class, false /*revoked*/),
+            ClassParameter.from(long.class, approvedMs),
+            ClassParameter.from(long.class, lastTimeConnectedMs),
+            ClassParameter.from(int.class, 0 /*systemDataSyncFlags*/));
+      }
+    } catch (ClassNotFoundException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
new file mode 100644
index 0000000..70e54b1
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/BluetoothConnectionManager.java
@@ -0,0 +1,139 @@
+package org.robolectric.shadows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages remote address connections for {@link ShadowBluetoothGatt} and {@link
+ * ShadowBluetoothGattServer}.
+ */
+final class BluetoothConnectionManager {
+
+  private static volatile BluetoothConnectionManager instance;
+
+  /** Connection metadata for Gatt Server and Client connections. */
+  private static class BluetoothConnectionMetadata {
+    boolean hasGattClientConnection = false;
+    boolean hasGattServerConnection = false;
+
+    void setHasGattClientConnection(boolean hasGattClientConnection) {
+      this.hasGattClientConnection = hasGattClientConnection;
+    }
+
+    void setHasGattServerConnection(boolean hasGattServerConnection) {
+      this.hasGattServerConnection = hasGattServerConnection;
+    }
+
+    boolean hasGattClientConnection() {
+      return hasGattClientConnection;
+    }
+
+    boolean hasGattServerConnection() {
+      return hasGattServerConnection;
+    }
+
+    boolean isConnected() {
+      return hasGattClientConnection || hasGattServerConnection;
+    }
+  }
+
+  private BluetoothConnectionManager() {}
+
+  static BluetoothConnectionManager getInstance() {
+    if (instance == null) {
+      synchronized (BluetoothConnectionManager.class) {
+        if (instance == null) {
+          instance = new BluetoothConnectionManager();
+        }
+      }
+    }
+    return instance;
+  }
+
+  /**
+   * Map representing remote address connections, mapping a remote address to a {@link
+   * BluetoothConnectionMetadata}.
+   */
+  private final Map<String, BluetoothConnectionMetadata> remoteAddressConnectionMap =
+      new HashMap<>();
+
+  /**
+   * Register a Gatt Client Connection. Intended for use by {@link
+   * ShadowBluetoothGatt#notifyConnection} when simulating a successful Gatt Client Connection.
+   */
+  void registerGattClientConnection(String remoteAddress) {
+    if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+      remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+    }
+    remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(true);
+  }
+
+  /**
+   * Unregister a Gatt Client Connection. Intended for use by {@link
+   * ShadowBluetoothGatt#notifyDisconnection} when simulating a successful Gatt client
+   * disconnection.
+   */
+  void unregisterGattClientConnection(String remoteAddress) {
+    if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+      remoteAddressConnectionMap.get(remoteAddress).setHasGattClientConnection(false);
+    }
+  }
+
+  /**
+   * Register a Gatt Server Connection. Intended for use by {@link
+   * ShadowBluetoothGattServer#notifyConnection} when simulating a successful Gatt server
+   * connection.
+   */
+  void registerGattServerConnection(String remoteAddress) {
+    if (!remoteAddressConnectionMap.containsKey(remoteAddress)) {
+      remoteAddressConnectionMap.put(remoteAddress, new BluetoothConnectionMetadata());
+    }
+    remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(true);
+  }
+
+  /**
+   * Unregister a Gatt Server Connection. Intended for use by {@link
+   * ShadowBluetoothGattServer#notifyDisconnection} when simulating a successful Gatt server
+   * disconnection.
+   */
+  void unregisterGattServerConnection(String remoteAddress) {
+    if (remoteAddressConnectionMap.containsKey(remoteAddress)) {
+      remoteAddressConnectionMap.get(remoteAddress).setHasGattServerConnection(false);
+    }
+  }
+
+  /**
+   * Returns true if remote address has an active gatt client connection.
+   *
+   * @param remoteAddress remote address
+   */
+  boolean hasGattClientConnection(String remoteAddress) {
+    return remoteAddressConnectionMap.containsKey(remoteAddress)
+        && remoteAddressConnectionMap.get(remoteAddress).hasGattClientConnection();
+  }
+
+  /**
+   * Returns true if remote address has an active gatt server connection.
+   *
+   * @param remoteAddress remote address
+   */
+  boolean hasGattServerConnection(String remoteAddress) {
+    return remoteAddressConnectionMap.containsKey(remoteAddress)
+        && remoteAddressConnectionMap.get(remoteAddress).hasGattServerConnection();
+  }
+
+  /**
+   * Returns true if remote address has an active connection.
+   *
+   * @param remoteAddress remote address
+   */
+  boolean isConnected(String remoteAddress) {
+    return remoteAddressConnectionMap.containsKey(remoteAddress)
+        && remoteAddressConnectionMap.get(remoteAddress).isConnected();
+  }
+
+  /** Clears all connection information */
+  void resetConnections() {
+    this.remoteAddressConnectionMap.clear();
+  }
+}
\ No newline at end of file
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
new file mode 100644
index 0000000..597aef2
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityLteBuilder.java
@@ -0,0 +1,170 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.ClosedSubscriberGroupInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellIdentityLteBuilder {
+
+  @Nullable private String mcc = null;
+  @Nullable private String mnc = null;
+  private int ci = CellInfo.UNAVAILABLE;
+  private int pci = CellInfo.UNAVAILABLE;
+  private int tac = CellInfo.UNAVAILABLE;
+  private int earfcn = CellInfo.UNAVAILABLE;
+  private int[] bands = new int[0];
+  private int bandwidth = CellInfo.UNAVAILABLE;
+  @Nullable private String alphal = null;
+  @Nullable private String alphas = null;
+  private List<String> additionalPlmns = new ArrayList<>();
+
+  private CellIdentityLteBuilder() {}
+
+  public static CellIdentityLteBuilder newBuilder() {
+    return new CellIdentityLteBuilder();
+  }
+
+  protected static CellIdentityLte getDefaultInstance() {
+    return reflector(CellIdentityLteReflector.class).newCellIdentityLte();
+  }
+
+  public CellIdentityLteBuilder setMcc(String mcc) {
+    this.mcc = mcc;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setMnc(String mnc) {
+    this.mnc = mnc;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setCi(int ci) {
+    this.ci = ci;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setPci(int pci) {
+    this.pci = pci;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setTac(int tac) {
+    this.tac = tac;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setEarfcn(int earfcn) {
+    this.earfcn = earfcn;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setBands(int[] bands) {
+    this.bands = bands;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setBandwidth(int bandwidth) {
+    this.bandwidth = bandwidth;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setLongOperatorName(String longOperatorName) {
+    this.alphal = longOperatorName;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setShortOperatorName(String shortOperatorName) {
+    this.alphas = shortOperatorName;
+    return this;
+  }
+
+  public CellIdentityLteBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+    this.additionalPlmns = additionalPlmns;
+    return this;
+  }
+
+  public CellIdentityLte build() {
+    CellIdentityLteReflector cellIdentityLteReflector = reflector(CellIdentityLteReflector.class);
+    int apiLevel = RuntimeEnvironment.getApiLevel();
+    if (apiLevel < Build.VERSION_CODES.N) {
+      return cellIdentityLteReflector.newCellIdentityLte(
+          mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac);
+    } else if (apiLevel < Build.VERSION_CODES.P) {
+      return cellIdentityLteReflector.newCellIdentityLte(
+          mccOrMncToInt(mcc), mccOrMncToInt(mnc), ci, pci, tac, earfcn);
+    } else if (apiLevel < Build.VERSION_CODES.R) {
+      return cellIdentityLteReflector.newCellIdentityLte(
+          ci, pci, tac, earfcn, bandwidth, mcc, mnc, alphal, alphas);
+    } else {
+      return cellIdentityLteReflector.newCellIdentityLte(
+          ci,
+          pci,
+          tac,
+          earfcn,
+          bands,
+          bandwidth,
+          mcc,
+          mnc,
+          alphal,
+          alphas,
+          additionalPlmns,
+          /* csgInfo= */ null);
+    }
+  }
+
+  private static int mccOrMncToInt(@Nullable String mccOrMnc) {
+    return mccOrMnc == null ? CellInfo.UNAVAILABLE : Integer.parseInt(mccOrMnc);
+  }
+
+  @ForType(CellIdentityLte.class)
+  private interface CellIdentityLteReflector {
+    @Constructor
+    CellIdentityLte newCellIdentityLte();
+
+    @Constructor
+    CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac);
+
+    @Constructor
+    CellIdentityLte newCellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn);
+
+    @Constructor
+    CellIdentityLte newCellIdentityLte(
+        int ci,
+        int pci,
+        int tac,
+        int earfcn,
+        int bandwidth,
+        String mcc,
+        String mnc,
+        String alphal,
+        String alphas);
+
+    @Constructor
+    CellIdentityLte newCellIdentityLte(
+        int ci,
+        int pci,
+        int tac,
+        int earfcn,
+        int[] bands,
+        int bandwidth,
+        String mcc,
+        String mnc,
+        String alphal,
+        String alphas,
+        Collection<String> additionalPlmns,
+        ClosedSubscriberGroupInfo csgInfo);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
new file mode 100644
index 0000000..22a0e75
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellIdentityNrBuilder.java
@@ -0,0 +1,135 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfo;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellIdentityNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellIdentityNrBuilder {
+
+  private int pci = CellInfo.UNAVAILABLE;
+  private int tac = CellInfo.UNAVAILABLE;
+  private int nrarfcn = CellInfo.UNAVAILABLE;
+  private int[] bands = new int[0];
+  @Nullable private String mcc = null;
+  @Nullable private String mnc = null;
+  private long nci = CellInfo.UNAVAILABLE;
+  @Nullable private String alphal = null;
+  @Nullable private String alphas = null;
+  private List<String> additionalPlmns = new ArrayList<>();
+
+  private CellIdentityNrBuilder() {}
+
+  public static CellIdentityNrBuilder newBuilder() {
+    return new CellIdentityNrBuilder();
+  }
+
+  // An empty constructor is not available on Q.
+  @RequiresApi(Build.VERSION_CODES.R)
+  protected static CellIdentityNr getDefaultInstance() {
+    return reflector(CellIdentityNrReflector.class).newCellIdentityNr();
+  }
+
+  public CellIdentityNrBuilder setNci(long nci) {
+    this.nci = nci;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setPci(int pci) {
+    this.pci = pci;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setTac(int tac) {
+    this.tac = tac;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setNrarfcn(int nrarfcn) {
+    this.nrarfcn = nrarfcn;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setMcc(String mcc) {
+    this.mcc = mcc;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setMnc(String mnc) {
+    this.mnc = mnc;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setBands(int[] bands) {
+    this.bands = bands;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setLongOperatorName(String longOperatorName) {
+    this.alphal = longOperatorName;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setShortOperatorName(String shortOperatorName) {
+    this.alphas = shortOperatorName;
+    return this;
+  }
+
+  public CellIdentityNrBuilder setAdditionalPlmns(List<String> additionalPlmns) {
+    this.additionalPlmns = additionalPlmns;
+    return this;
+  }
+
+  public CellIdentityNr build() {
+    CellIdentityNrReflector cellIdentityReflector = reflector(CellIdentityNrReflector.class);
+    if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.R) {
+      return cellIdentityReflector.newCellIdentityNr(
+          pci, tac, nrarfcn, mcc, mnc, nci, alphal, alphas);
+    } else {
+      return cellIdentityReflector.newCellIdentityNr(
+          pci, tac, nrarfcn, bands, mcc, mnc, nci, alphal, alphas, additionalPlmns);
+    }
+  }
+
+  @ForType(CellIdentityNr.class)
+  private interface CellIdentityNrReflector {
+
+    @Constructor
+    CellIdentityNr newCellIdentityNr();
+
+    @Constructor
+    CellIdentityNr newCellIdentityNr(
+        int pci,
+        int tac,
+        int nrarfcn,
+        String mcc,
+        String mnc,
+        long nci,
+        String alphal,
+        String alphas);
+
+    @Constructor
+    CellIdentityNr newCellIdentityNr(
+        int pci,
+        int tac,
+        int nrarfcn,
+        int[] bands,
+        String mcc,
+        String mnc,
+        long nci,
+        String alphal,
+        String alphas,
+        Collection<String> additionalPlmns);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
new file mode 100644
index 0000000..6f3f934
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoLteBuilder.java
@@ -0,0 +1,143 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellIdentityLte;
+import android.telephony.CellInfo;
+import android.telephony.CellInfoLte;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+import org.robolectric.util.reflector.WithType;
+
+/** Builder for {@link android.telephony.CellInfoLte}. */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellInfoLteBuilder {
+
+  private boolean isRegistered = false;
+  private long timeStamp = 0L;
+  private int cellConnectionStatus = 0;
+  private CellIdentityLte cellIdentity;
+  private CellSignalStrengthLte cellSignalStrength;
+
+  private CellInfoLteBuilder() {}
+
+  public static CellInfoLteBuilder newBuilder() {
+    return new CellInfoLteBuilder();
+  }
+
+  public CellInfoLteBuilder setRegistered(boolean isRegistered) {
+    this.isRegistered = isRegistered;
+    return this;
+  }
+
+  public CellInfoLteBuilder setTimeStampNanos(long timeStamp) {
+    this.timeStamp = timeStamp;
+    return this;
+  }
+
+  public CellInfoLteBuilder setCellConnectionStatus(int cellConnectionStatus) {
+    this.cellConnectionStatus = cellConnectionStatus;
+    return this;
+  }
+
+  public CellInfoLteBuilder setCellIdentity(CellIdentityLte cellIdentity) {
+    this.cellIdentity = cellIdentity;
+    return this;
+  }
+
+  public CellInfoLteBuilder setCellSignalStrength(CellSignalStrengthLte cellSignalStrength) {
+    this.cellSignalStrength = cellSignalStrength;
+    return this;
+  }
+
+  public CellInfoLte build() {
+    int apiLevel = RuntimeEnvironment.getApiLevel();
+    if (cellIdentity == null) {
+      if (apiLevel > Build.VERSION_CODES.Q) {
+        cellIdentity = CellIdentityLteBuilder.getDefaultInstance();
+      } else {
+        cellIdentity = CellIdentityLteBuilder.newBuilder().build();
+      }
+    }
+    if (cellSignalStrength == null) {
+      cellSignalStrength = CellSignalStrengthLteBuilder.getDefaultInstance();
+    }
+    CellInfoLteReflector cellInfoLteReflector = reflector(CellInfoLteReflector.class);
+    if (apiLevel < Build.VERSION_CODES.TIRAMISU) {
+      CellInfoLte cellInfo = cellInfoLteReflector.newCellInfoLte();
+      cellInfoLteReflector = reflector(CellInfoLteReflector.class, cellInfo);
+      cellInfoLteReflector.setCellIdentity(cellIdentity);
+      cellInfoLteReflector.setCellSignalStrength(cellSignalStrength);
+      CellInfoReflector cellInfoReflector = reflector(CellInfoReflector.class, cellInfo);
+      cellInfoReflector.setTimeStamp(timeStamp);
+      if (apiLevel <= Build.VERSION_CODES.KITKAT) {
+        cellInfoReflector.setRegisterd(isRegistered);
+      } else {
+        cellInfoReflector.setRegistered(isRegistered);
+      }
+      if (apiLevel > Build.VERSION_CODES.O_MR1) {
+        cellInfoReflector.setCellConnectionStatus(cellConnectionStatus);
+      }
+      return cellInfo;
+    } else {
+      try {
+        // This reflection is highly brittle but there is currently no choice as CellConfigLte is
+        // entirely @hide.
+        Class cellConfigLteClass = Class.forName("android.telephony.CellConfigLte");
+        return cellInfoLteReflector.newCellInfoLte(
+            cellConnectionStatus,
+            isRegistered,
+            timeStamp,
+            cellIdentity,
+            cellSignalStrength,
+            ReflectionHelpers.callConstructor(cellConfigLteClass));
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  @ForType(CellInfoLte.class)
+  private interface CellInfoLteReflector {
+    @Constructor
+    CellInfoLte newCellInfoLte();
+
+    @Constructor
+    CellInfoLte newCellInfoLte(
+        int cellConnectionStatus,
+        boolean isRegistered,
+        long timeStamp,
+        CellIdentityLte cellIdentity,
+        CellSignalStrengthLte cellSignalStrength,
+        @WithType("android.telephony.CellConfigLte") Object cellConfigLte);
+
+    @Accessor("mCellIdentityLte")
+    void setCellIdentity(CellIdentityLte cellIdentity);
+
+    @Accessor("mCellSignalStrengthLte")
+    void setCellSignalStrength(CellSignalStrengthLte cellSignalStrength);
+  }
+
+  @ForType(CellInfo.class)
+  private interface CellInfoReflector {
+
+    // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/kitkat-release/telephony/java/android/telephony/CellInfo.java#79
+    @Accessor("mRegistered")
+    void setRegisterd(boolean registered); // NOTYPO
+
+    @Accessor("mRegistered")
+    void setRegistered(boolean registered);
+
+    @Accessor("mTimeStamp")
+    void setTimeStamp(long registered);
+
+    @Accessor("mCellConnectionStatus")
+    void setCellConnectionStatus(int cellConnectionStatus);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
new file mode 100644
index 0000000..7819524
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellInfoNrBuilder.java
@@ -0,0 +1,93 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.telephony.CellIdentityNr;
+import android.telephony.CellInfoNr;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellInfoNr}. */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellInfoNrBuilder {
+
+  private boolean isRegistered = false;
+  private long timeStamp = 0L;
+  private int cellConnectionStatus = 0;
+  private CellIdentityNr cellIdentity;
+  private CellSignalStrengthNr cellSignalStrength;
+
+  private CellInfoNrBuilder() {}
+
+  public static CellInfoNrBuilder newBuilder() {
+    return new CellInfoNrBuilder();
+  }
+
+  public CellInfoNrBuilder setRegistered(boolean isRegistered) {
+    this.isRegistered = isRegistered;
+    return this;
+  }
+
+  public CellInfoNrBuilder setTimeStampNanos(long timeStamp) {
+    this.timeStamp = timeStamp;
+    return this;
+  }
+
+  public CellInfoNrBuilder setCellConnectionStatus(int cellConnectionStatus) {
+    this.cellConnectionStatus = cellConnectionStatus;
+    return this;
+  }
+
+  public CellInfoNrBuilder setCellIdentity(CellIdentityNr cellIdentity) {
+    this.cellIdentity = cellIdentity;
+    return this;
+  }
+
+  public CellInfoNrBuilder setCellSignalStrength(CellSignalStrengthNr cellSignalStrength) {
+    this.cellSignalStrength = cellSignalStrength;
+    return this;
+  }
+
+  public CellInfoNr build() {
+    if (cellIdentity == null) {
+      cellIdentity = CellIdentityNrBuilder.getDefaultInstance();
+    }
+    if (cellSignalStrength == null) {
+      cellSignalStrength = CellSignalStrengthNrBuilder.getDefaultInstance();
+    }
+    // CellInfoNr has no default constructor below T so we write it to a Parcel.
+    if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.TIRAMISU) {
+      Parcel p = Parcel.obtain();
+      p.writeInt(/* CellInfo#TYPE_NR */ 6);
+      p.writeInt(isRegistered ? 1 : 0);
+      p.writeLong(timeStamp);
+      p.writeInt(cellConnectionStatus);
+      cellIdentity.writeToParcel(p, 0);
+      cellSignalStrength.writeToParcel(p, 0);
+      p.setDataPosition(0);
+      CellInfoNr cellInfoNr = CellInfoNr.CREATOR.createFromParcel(p);
+      p.recycle();
+      return cellInfoNr;
+    } else {
+      return reflector(CellInfoNrReflector.class)
+          .newCellInfoNr(
+              cellConnectionStatus, isRegistered, timeStamp, cellIdentity, cellSignalStrength);
+    }
+  }
+
+  @ForType(CellInfoNr.class)
+  private interface CellInfoNrReflector {
+    @Constructor
+    CellInfoNr newCellInfoNr(
+        int cellConnectionStatus,
+        boolean isRegistered,
+        long timeStamp,
+        CellIdentityNr cellIdentity,
+        CellSignalStrengthNr cellSignalStrength);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
new file mode 100644
index 0000000..9b5d1a1
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthLteBuilder.java
@@ -0,0 +1,96 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthLte;
+import androidx.annotation.RequiresApi;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthLte} */
+@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+public class CellSignalStrengthLteBuilder {
+
+  private int rssi = CellInfo.UNAVAILABLE;
+  private int rsrp = CellInfo.UNAVAILABLE;
+  private int rsrq = CellInfo.UNAVAILABLE;
+  private int rssnr = CellInfo.UNAVAILABLE;
+  private int cqiTableIndex = CellInfo.UNAVAILABLE;
+  private int cqi = CellInfo.UNAVAILABLE;
+  private int timingAdvance = CellInfo.UNAVAILABLE;
+
+  private CellSignalStrengthLteBuilder() {}
+
+  public static CellSignalStrengthLteBuilder newBuilder() {
+    return new CellSignalStrengthLteBuilder();
+  }
+
+  protected static CellSignalStrengthLte getDefaultInstance() {
+    return reflector(CellSignalStrengthLteReflector.class).newCellSignalStrength();
+  }
+
+  /** This is equivalent to {@code signalStrength} pre SDK Q. */
+  public CellSignalStrengthLteBuilder setRssi(int rssi) {
+    this.rssi = rssi;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setRsrp(int rsrp) {
+    this.rsrp = rsrp;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setRsrq(int rsrq) {
+    this.rsrq = rsrq;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setRssnr(int rssnr) {
+    this.rssnr = rssnr;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setCqiTableIndex(int cqiTableIndex) {
+    this.cqiTableIndex = cqiTableIndex;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setCqi(int cqi) {
+    this.cqi = cqi;
+    return this;
+  }
+
+  public CellSignalStrengthLteBuilder setTimingAdvance(int timingAdvance) {
+    this.timingAdvance = timingAdvance;
+    return this;
+  }
+
+  public CellSignalStrengthLte build() {
+    CellSignalStrengthLteReflector cellSignalStrengthReflector =
+        reflector(CellSignalStrengthLteReflector.class);
+    if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.S) {
+      return cellSignalStrengthReflector.newCellSignalStrength(
+          rssi, rsrp, rsrq, rssnr, cqi, timingAdvance);
+    } else {
+      return cellSignalStrengthReflector.newCellSignalStrength(
+          rssi, rsrp, rsrq, rssnr, cqiTableIndex, cqi, timingAdvance);
+    }
+  }
+
+  @ForType(CellSignalStrengthLte.class)
+  private interface CellSignalStrengthLteReflector {
+    @Constructor
+    CellSignalStrengthLte newCellSignalStrength();
+
+    @Constructor
+    CellSignalStrengthLte newCellSignalStrength(
+        int rssi, int rsrp, int rsrq, int rssnr, int cqi, int timingAdvance);
+
+    @Constructor
+    CellSignalStrengthLte newCellSignalStrength(
+        int rssi, int rsrp, int rsrq, int rssnr, int cqiTableIndex, int cqi, int timingAdvance);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
new file mode 100644
index 0000000..4f3f859
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/CellSignalStrengthNrBuilder.java
@@ -0,0 +1,140 @@
+package org.robolectric.shadows;
+
+import static org.robolectric.util.reflector.Reflector.reflector;
+
+import android.os.Build;
+import android.telephony.CellInfo;
+import android.telephony.CellSignalStrengthNr;
+import androidx.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.List;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.util.reflector.Constructor;
+import org.robolectric.util.reflector.ForType;
+
+/** Builder for {@link android.telephony.CellSignalStrengthNr} */
+@RequiresApi(Build.VERSION_CODES.Q)
+public class CellSignalStrengthNrBuilder {
+
+  private int csiRrsp = CellInfo.UNAVAILABLE;
+  private int csiRsrq = CellInfo.UNAVAILABLE;
+  private int csiSinr = CellInfo.UNAVAILABLE;
+  private int csiCqiTableIndex = CellInfo.UNAVAILABLE;
+  private List<Byte> csiCqiReport = new ArrayList<>();
+  private int ssRsrp = CellInfo.UNAVAILABLE;
+  private int ssRsrq = CellInfo.UNAVAILABLE;
+  private int ssSinr = CellInfo.UNAVAILABLE;
+  private int timingAdvance = CellInfo.UNAVAILABLE;
+
+  private CellSignalStrengthNrBuilder() {}
+
+  public static CellSignalStrengthNrBuilder newBuilder() {
+    return new CellSignalStrengthNrBuilder();
+  }
+
+  protected static CellSignalStrengthNr getDefaultInstance() {
+    return reflector(CellSignalStrengthNrReflector.class).newCellSignalStrengthNr();
+  }
+
+  public CellSignalStrengthNrBuilder setCsiRsrp(int csiRrsp) {
+    this.csiRrsp = csiRrsp;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setCsiRsrq(int csiRsrq) {
+    this.csiRsrq = csiRsrq;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setCsiSinr(int csiSinr) {
+    this.csiSinr = csiSinr;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setCsiCqiTableIndex(int csiCqiTableIndex) {
+    this.csiCqiTableIndex = csiCqiTableIndex;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setCsiCqiReport(List<Byte> csiCqiReport) {
+    this.csiCqiReport = csiCqiReport;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setSsRsrp(int ssRsrp) {
+    this.ssRsrp = ssRsrp;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setSsRsrq(int ssRsrq) {
+    this.ssRsrq = ssRsrq;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setSsSinr(int ssSinr) {
+    this.ssSinr = ssSinr;
+    return this;
+  }
+
+  public CellSignalStrengthNrBuilder setTimingAdvance(int timingAdvance) {
+    this.timingAdvance = timingAdvance;
+    return this;
+  }
+
+  public CellSignalStrengthNr build() {
+    CellSignalStrengthNrReflector cellSignalStrengthReflector =
+        reflector(CellSignalStrengthNrReflector.class);
+    if (RuntimeEnvironment.getApiLevel() < Build.VERSION_CODES.TIRAMISU) {
+      return cellSignalStrengthReflector.newCellSignalStrengthNr(
+          csiRrsp, csiRsrq, csiSinr, ssRsrp, ssRsrq, ssSinr);
+    } else if (RuntimeEnvironment.getApiLevel() == Build.VERSION_CODES.TIRAMISU) {
+      return cellSignalStrengthReflector.newCellSignalStrengthNr(
+          csiRrsp, csiRsrq, csiSinr, csiCqiTableIndex, csiCqiReport, ssRsrp, ssRsrq, ssSinr);
+    } else {
+      return cellSignalStrengthReflector.newCellSignalStrengthNr(
+          csiRrsp,
+          csiRsrq,
+          csiSinr,
+          csiCqiTableIndex,
+          csiCqiReport,
+          ssRsrp,
+          ssRsrq,
+          ssSinr,
+          timingAdvance);
+    }
+  }
+
+  @ForType(CellSignalStrengthNr.class)
+  private interface CellSignalStrengthNrReflector {
+
+    @Constructor
+    CellSignalStrengthNr newCellSignalStrengthNr();
+
+    @Constructor
+    CellSignalStrengthNr newCellSignalStrengthNr(
+        int csRsrp, int csiRsrq, int csiSinr, int ssRsrp, int ssRsrq, int ssSinr);
+
+    @Constructor
+    CellSignalStrengthNr newCellSignalStrengthNr(
+        int csRsrp,
+        int csiRsrq,
+        int csiSinr,
+        int csiCqiTableIndex,
+        List<Byte> csiCqiReport,
+        int ssRsrp,
+        int ssRsrq,
+        int ssSinr);
+
+    @Constructor
+    CellSignalStrengthNr newCellSignalStrengthNr(
+        int csRsrp,
+        int csiRsrq,
+        int csiSinr,
+        int csiCqiTableIndex,
+        List<Byte> csiCqiReport,
+        int ssRsrp,
+        int ssRsrq,
+        int ssSinr,
+        int timingAdvance);
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
index 841f7d5..4593402 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/MediaCodecInfoBuilder.java
@@ -10,6 +10,7 @@
 import android.media.MediaCodecInfo.EncoderCapabilities;
 import android.media.MediaCodecInfo.VideoCapabilities;
 import android.media.MediaFormat;
+import android.util.Range;
 import com.google.common.base.Preconditions;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.util.ReflectionHelpers;
@@ -266,6 +267,17 @@
       void setFlagsSupported(int flagsSupported);
     }
 
+    /** Accessor interface for {@link VideoCapabilities}'s internals. */
+    @ForType(VideoCapabilities.class)
+    interface VideoCapabilitiesReflector {
+
+      @Accessor("mWidthRange")
+      void setWidthRange(Range<Integer> range);
+
+      @Accessor("mHeightRange")
+      void setHeightRange(Range<Integer> range);
+    }
+
     public CodecCapabilities build() {
       Preconditions.checkNotNull(mediaFormat, "mediaFormat is not set.");
       Preconditions.checkNotNull(profileLevels, "profileLevels is not set.");
@@ -298,6 +310,16 @@
 
       if (isVideoCodec) {
         VideoCapabilities videoCaps = createDefaultVideoCapabilities(caps, mediaFormat);
+        VideoCapabilitiesReflector videoCapsReflector =
+            Reflector.reflector(VideoCapabilitiesReflector.class, videoCaps);
+        if (mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
+          videoCapsReflector.setWidthRange(
+              new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_WIDTH)));
+        }
+        if (mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
+          videoCapsReflector.setHeightRange(
+              new Range<>(1, mediaFormat.getInteger(MediaFormat.KEY_HEIGHT)));
+        }
         capsReflector.setVideoCaps(videoCaps);
       } else {
         AudioCapabilities audioCaps = createDefaultAudioCapabilities(caps, mediaFormat);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
index 5da1409..d23045b 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ResourceModeShadowPicker.java
@@ -1,6 +1,7 @@
 package org.robolectric.shadows;
 
 import android.os.Build;
+import android.os.Build.VERSION_CODES;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.shadow.api.ShadowPicker;
 
@@ -10,6 +11,7 @@
   private Class<? extends T> binaryShadowClass;
   private Class<? extends T> binary9ShadowClass;
   private Class<? extends T> binary10ShadowClass;
+  private Class<? extends T> binary14ShadowClass;
 
   public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
       Class<? extends T> binaryShadowClass,
@@ -18,16 +20,19 @@
     this.binaryShadowClass = binaryShadowClass;
     this.binary9ShadowClass = binary9ShadowClass;
     this.binary10ShadowClass = binary9ShadowClass;
+    this.binary14ShadowClass = binary9ShadowClass;
   }
 
   public ResourceModeShadowPicker(Class<? extends T> legacyShadowClass,
           Class<? extends T> binaryShadowClass,
           Class<? extends T> binary9ShadowClass,
-          Class<? extends T> binary10ShadowClass) {
+          Class<? extends T> binary10ShadowClass,
+          Class<? extends T> binary14ShadowClass) {
     this.legacyShadowClass = legacyShadowClass;
     this.binaryShadowClass = binaryShadowClass;
     this.binary9ShadowClass = binary9ShadowClass;
     this.binary10ShadowClass = binary10ShadowClass;
+    this.binary14ShadowClass = binary14ShadowClass;
   }
 
   @Override
@@ -35,10 +40,11 @@
     if (ShadowAssetManager.useLegacy()) {
       return legacyShadowClass;
     } else {
-      if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
+      if (RuntimeEnvironment.getApiLevel() > VERSION_CODES.TIRAMISU) {
+        return binary14ShadowClass;
+      } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
         return binary10ShadowClass;
-      } else
-      if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
+      } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
         return binary9ShadowClass;
       } else {
         return binaryShadowClass;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
index 47f7306..1d575a4 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivity.java
@@ -426,10 +426,10 @@
 
   @Implementation
   protected void runOnUiThread(Runnable action) {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
+    } else {
+      reflector(DirectActivityReflector.class, realActivity).runOnUiThread(action);
     }
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
index 883dd2c..70464bf 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowActivityThread.java
@@ -26,7 +26,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import javax.annotation.Nonnull;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
@@ -34,6 +33,7 @@
 import org.robolectric.annotation.RealObject;
 import org.robolectric.annotation.ReflectorObject;
 import org.robolectric.annotation.Resetter;
+import org.robolectric.util.Logger;
 import org.robolectric.util.ReflectionHelpers;
 import org.robolectric.util.reflector.Accessor;
 import org.robolectric.util.reflector.ForType;
@@ -275,7 +275,12 @@
   @Resetter
   public static void reset() {
     Object activityThread = RuntimeEnvironment.getActivityThread();
-    Objects.requireNonNull(activityThread, "ShadowActivityThread.reset: ActivityThread not set");
-    reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+    if (activityThread == null) {
+      Logger.warn(
+          "RuntimeEnvironment.getActivityThread() is null, an error likely occurred during test"
+              + " initialization.");
+    } else {
+      reflector(_ActivityThread_.class, activityThread).getActivities().clear();
+    }
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscApkAssets9.java
old mode 100755
new mode 100644
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager.java
old mode 100755
new mode 100644
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
new file mode 100644
index 0000000..8771d6a
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowArscAssetManager14.java
@@ -0,0 +1,72 @@
+package org.robolectric.shadows;
+
+
+import android.annotation.Nullable;
+import android.content.res.AssetManager;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+// TODO: update path to released version.
+// transliterated from
+// https://android.googlesource.com/platform/frameworks/base/+/android-10.0.0_rXX/core/jni/android_util_AssetManager.cpp
+
+@Implements(
+    value = AssetManager.class,
+    minSdk = ShadowBuild.UPSIDE_DOWN_CAKE,
+    shadowPicker = ShadowAssetManager.Picker.class)
+@SuppressWarnings("NewApi")
+public class ShadowArscAssetManager14 extends ShadowArscAssetManager10 {
+
+  // static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint
+  // mnc,
+  //                                    jstring locale, jint orientation, jint touchscreen, jint
+  // density,
+  //                                    jint keyboard, jint keyboard_hidden, jint navigation,
+  //                                    jint screen_width, jint screen_height,
+  //                                    jint smallest_screen_width_dp, jint screen_width_dp,
+  //                                    jint screen_height_dp, jint screen_layout, jint ui_mode,
+  //                                    jint color_mode, jint major_version) {
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+  protected static void nativeSetConfiguration(
+      long ptr,
+      int mcc,
+      int mnc,
+      @Nullable String locale,
+      int orientation,
+      int touchscreen,
+      int density,
+      int keyboard,
+      int keyboard_hidden,
+      int navigation,
+      int screen_width,
+      int screen_height,
+      int smallest_screen_width_dp,
+      int screen_width_dp,
+      int screen_height_dp,
+      int screen_layout,
+      int ui_mode,
+      int color_mode,
+      int grammaticalGender, // ignore for now?
+      int major_version) {
+    ShadowArscAssetManager10.nativeSetConfiguration(
+        ptr,
+        mcc,
+        mnc,
+        locale,
+        orientation,
+        touchscreen,
+        density,
+        keyboard,
+        keyboard_hidden,
+        navigation,
+        screen_width,
+        screen_height,
+        smallest_screen_width_dp,
+        screen_width_dp,
+        screen_height_dp,
+        screen_layout,
+        ui_mode,
+        color_mode,
+        major_version);
+  }
+}
+// namespace android
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
index 1f6e40d..19c5196 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAssetManager.java
@@ -25,7 +25,8 @@
           ShadowLegacyAssetManager.class,
           ShadowArscAssetManager.class,
           ShadowArscAssetManager9.class,
-          ShadowArscAssetManager10.class);
+          ShadowArscAssetManager10.class,
+          ShadowArscAssetManager14.class);
     }
   }
 
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 2234351..5f2d4fd 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioManager.java
@@ -93,6 +93,7 @@
   private ImmutableList<Object> defaultDevicesForAttributes = ImmutableList.of();
   private List<AudioDeviceInfo> inputDevices = new ArrayList<>();
   private List<AudioDeviceInfo> outputDevices = new ArrayList<>();
+  private List<AudioDeviceInfo> availableCommunicationDevices = new ArrayList<>();
   private AudioDeviceInfo communicationDevice = null;
 
   public ShadowAudioManager() {
@@ -451,6 +452,23 @@
   }
 
   /**
+   * Sets the list of available communication devices represented by {@link AudioDeviceInfo}.
+   *
+   * <p>The previous list of communication 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)}.
+   */
+  @TargetApi(VERSION_CODES.S)
+  public void setAvailableCommunicationDevices(
+      List<AudioDeviceInfo> availableCommunicationDevices) {
+    this.availableCommunicationDevices = new ArrayList<>(availableCommunicationDevices);
+  }
+
+  /**
    * Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if
    * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}.
    */
@@ -497,6 +515,36 @@
   }
 
   /**
+   * Adds an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+   * AudioDeviceCallback} if the device was not present before and indicated by {@code
+   * notifyAudioDeviceCallbacks}.
+   */
+  @TargetApi(VERSION_CODES.S)
+  public void addAvailableCommunicationDevice(
+      AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed =
+        !this.availableCommunicationDevices.contains(communicationDevice)
+            && this.availableCommunicationDevices.add(communicationDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ true);
+    }
+  }
+
+  /**
+   * Removes an available communication {@link AudioDeviceInfo} and notifies the list of {@link
+   * AudioDeviceCallback} if the device was present before and indicated by {@code
+   * notifyAudioDeviceCallbacks}.
+   */
+  @TargetApi(VERSION_CODES.S)
+  public void removeAvailableCommunicationDevice(
+      AudioDeviceInfo communicationDevice, boolean notifyAudioDeviceCallbacks) {
+    boolean changed = this.availableCommunicationDevices.remove(communicationDevice);
+    if (changed && notifyAudioDeviceCallbacks) {
+      notifyAudioDeviceCallbacks(ImmutableList.of(communicationDevice), /* added= */ false);
+    }
+  }
+
+  /**
    * Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set
    * of connected audio devices.
    *
@@ -504,8 +552,10 @@
    *
    * @see #addInputDevice(AudioDeviceInfo, boolean)
    * @see #addOutputDevice(AudioDeviceInfo, boolean)
+   * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
    * @see #removeInputDevice(AudioDeviceInfo, boolean)
    * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+   * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
    */
   @Implementation(minSdk = M)
   protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) {
@@ -520,8 +570,10 @@
    *
    * @see #addInputDevice(AudioDeviceInfo, boolean)
    * @see #addOutputDevice(AudioDeviceInfo, boolean)
+   * @see #addAvailableCommunicationDevice(AudioDeviceInfo, boolean)
    * @see #removeInputDevice(AudioDeviceInfo, boolean)
    * @see #removeOutputDevice(AudioDeviceInfo, boolean)
+   * @see #removeAvailableCommunicationDevice(AudioDeviceInfo, boolean)
    */
   @Implementation(minSdk = M)
   protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) {
@@ -563,6 +615,11 @@
     this.communicationDevice = null;
   }
 
+  @Implementation(minSdk = S)
+  protected List<AudioDeviceInfo> getAvailableCommunicationDevices() {
+    return availableCommunicationDevices;
+  }
+
   @Implementation(minSdk = M)
   public AudioDeviceInfo[] getDevices(int flags) {
     List<AudioDeviceInfo> result = new ArrayList<>();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
index 5b05140..c1e78be 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioSystem.java
@@ -3,10 +3,23 @@
 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 com.google.common.base.Preconditions.checkNotNull;
 
+import android.annotation.NonNull;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
 import android.media.AudioSystem;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.Table;
+import com.google.common.collect.Tables;
+import java.util.Optional;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
 
 /** Shadow for {@link AudioSystem}. */
 @Implements(value = AudioSystem.class, isInAndroidSdk = false)
@@ -17,6 +30,33 @@
   private static final int MAX_SAMPLE_RATE = 192000;
   private static final int MIN_SAMPLE_RATE = 4000;
 
+  /**
+   * Table to store key-pair of {@link AudioFormat} and {@link AudioAttributes#getUsage()} with
+   * value of support for Direct Playback. Used with {@link #setDirectPlaybackSupport(AudioFormat,
+   * AudioAttributes, int)}, and {@link #getDirectPlaybackSupport(AudioFormat, AudioAttributes)}.
+   */
+  private static final Table<AudioFormat, Integer, Integer> directPlaybackSupportTable =
+      Tables.synchronizedTable(HashBasedTable.create());
+  /**
+   * Table to store pair of {@link OffloadSupportFormat} and {@link
+   * AudioAttributes#getVolumeControlStream()} with a value of Offload Playback support. Used with
+   * {@link #native_get_offload_support}. The table uses {@link OffloadSupportFormat} rather than
+   * {@link AudioFormat} because {@link #native_get_offload_support} does not pass all the fields
+   * needed to reliably reconstruct {@link AudioFormat} instances.
+   */
+  private static final Table<OffloadSupportFormat, Integer, Integer> offloadPlaybackSupportTable =
+      Tables.synchronizedTable(HashBasedTable.create());
+
+  /**
+   * Multimap to store whether a pair of {@link OffloadSupportFormat} and {@link
+   * AudioAttributes#getVolumeControlStream()} ()} support offloaded playback. Used with {@link
+   * #native_is_offload_supported}. The map uses {@link OffloadSupportFormat} keys rather than
+   * {@link AudioFormat} because {@link #native_is_offload_supported} does not pass all the fields
+   * needed to reliably reconstruct {@link AudioFormat} instances.
+   */
+  private static final Multimap<OffloadSupportFormat, Integer> offloadSupportedMap =
+      Multimaps.synchronizedMultimap(HashMultimap.create());
+
   @Implementation(minSdk = S)
   protected static int native_getMaxChannelCount() {
     return MAX_CHANNEL_COUNT;
@@ -38,4 +78,156 @@
     // https://cs.android.com/android/platform/superproject/+/master:system/media/audio/include/system/audio-base.h;l=197;drc=c84ca89fa5d660046364897482b202c797c8595e
     return 8;
   }
+
+  /**
+   * Sets direct playback support for a key-pair of {@link AudioFormat} and {@link AudioAttributes}.
+   * As a result, calling {@link #getDirectPlaybackSupport} with the same pair of {@link
+   * AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+   *
+   * @param format the audio format (codec, sample rate, channels)
+   * @param attr the {@link AudioAttributes} to be used for playback
+   * @param directPlaybackSupport the level of direct playback support to save for the format and
+   *     attribute pair. Must be one of {@link AudioSystem#DIRECT_NOT_SUPPORTED}, {@link
+   *     AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link AudioSystem#OFFLOAD_SUPPORTED}, {@link
+   *     AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}, or a combination of {@link
+   *     AudioSystem#DIRECT_OFFLOAD_SUPPORTED}, {@link AudioSystem#DIRECT_OFFLOAD_GAPLESS_SUPPORTED}
+   *     and {@link AudioSystem#DIRECT_BITSTREAM_SUPPORTED}
+   */
+  public static void setDirectPlaybackSupport(
+      @NonNull AudioFormat format, @NonNull AudioAttributes attr, int directPlaybackSupport) {
+    checkNotNull(format, "Illegal null AudioFormat");
+    checkNotNull(attr, "Illegal null AudioAttributes");
+    directPlaybackSupportTable.put(format, attr.getUsage(), directPlaybackSupport);
+  }
+
+  /**
+   * Retrieves the stored direct playback support for the {@link AudioFormat} and {@link
+   * AudioAttributes}. If no value was stored for the key-pair then {@link
+   * AudioSystem#DIRECT_NOT_SUPPORTED} is returned.
+   *
+   * @param format the audio format (codec, sample rate, channels) to be used for playback
+   * @param attr the {@link AudioAttributes} to be used for playback
+   * @return the level of direct playback playback support for the format and attributes.
+   */
+  @Implementation(minSdk = TIRAMISU)
+  protected static int getDirectPlaybackSupport(
+      @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+    return Optional.ofNullable(directPlaybackSupportTable.get(format, attr.getUsage()))
+        .orElse(AudioSystem.DIRECT_NOT_SUPPORTED);
+  }
+
+  /**
+   * Sets offload playback support for a key-pair of {@link AudioFormat} and {@link
+   * AudioAttributes}. As a result, calling {@link AudioSystem#getOffloadSupport} with the same pair
+   * of {@link AudioFormat} and {@link AudioAttributes} values will return the cached support value.
+   *
+   * @param format the audio format (codec, sample rate, channels)
+   * @param attr the {@link AudioAttributes} to be used for playback
+   * @param offloadSupport the level of offload playback support to save for the format and
+   *     attribute pair. Must be one of {@link AudioSystem#OFFLOAD_NOT_SUPPORTED}, {@link
+   *     AudioSystem#OFFLOAD_SUPPORTED} or {@link AudioSystem#OFFLOAD_GAPLESS_SUPPORTED}.
+   */
+  public static void setOffloadPlaybackSupport(
+      @NonNull AudioFormat format, @NonNull AudioAttributes attr, int offloadSupport) {
+    checkNotNull(format, "Illegal null AudioFormat");
+    checkNotNull(attr, "Illegal null AudioAttributes");
+    offloadPlaybackSupportTable.put(
+        new OffloadSupportFormat(
+            format.getEncoding(),
+            format.getSampleRate(),
+            format.getChannelMask(),
+            format.getChannelIndexMask()),
+        attr.getVolumeControlStream(),
+        offloadSupport);
+  }
+
+  /**
+   * Sets whether offload playback is supported for a key-pair of {@link AudioFormat} and {@link
+   * AudioAttributes}. As a result, calling {@link AudioSystem#isOffloadSupported} with the same
+   * pair of {@link AudioFormat} and {@link AudioAttributes} values will return {@code supported}.
+   *
+   * @param format the audio format (codec, sample rate, channels)
+   * @param attr the {@link AudioAttributes} to be used for playback
+   */
+  public static void setOffloadSupported(
+      @NonNull AudioFormat format, @NonNull AudioAttributes attr, boolean supported) {
+    OffloadSupportFormat offloadSupportFormat =
+        new OffloadSupportFormat(
+            format.getEncoding(),
+            format.getSampleRate(),
+            format.getChannelMask(),
+            format.getChannelIndexMask());
+    if (supported) {
+      offloadSupportedMap.put(offloadSupportFormat, attr.getVolumeControlStream());
+    } else {
+      offloadSupportedMap.remove(offloadSupportFormat, attr.getVolumeControlStream());
+    }
+  }
+
+  @Implementation(minSdk = Q, maxSdk = R)
+  protected static boolean native_is_offload_supported(
+      int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+    return offloadSupportedMap.containsEntry(
+        new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask), streamType);
+  }
+
+  @Implementation(minSdk = S)
+  protected static int native_get_offload_support(
+      int encoding, int sampleRate, int channelMask, int channelIndexMask, int streamType) {
+    return Optional.ofNullable(
+            offloadPlaybackSupportTable.get(
+                new OffloadSupportFormat(encoding, sampleRate, channelMask, channelIndexMask),
+                streamType))
+        .orElse(AudioSystem.OFFLOAD_NOT_SUPPORTED);
+  }
+
+  @Resetter
+  public static void reset() {
+    directPlaybackSupportTable.clear();
+    offloadPlaybackSupportTable.clear();
+    offloadSupportedMap.clear();
+  }
+
+  /**
+   * Struct to hold specific values from {@link AudioFormat} which are used in {@link
+   * #native_get_offload_support} and {@link #native_is_offload_supported}.
+   */
+  private static class OffloadSupportFormat {
+    public final int encoding;
+    public final int sampleRate;
+    public final int channelMask;
+    public final int channelIndexMask;
+
+    public OffloadSupportFormat(
+        int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+      this.encoding = encoding;
+      this.sampleRate = sampleRate;
+      this.channelMask = channelMask;
+      this.channelIndexMask = channelIndexMask;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof OffloadSupportFormat)) {
+        return false;
+      }
+      OffloadSupportFormat that = (OffloadSupportFormat) o;
+      return encoding == that.encoding
+          && sampleRate == that.sampleRate
+          && channelMask == that.channelMask
+          && channelIndexMask == that.channelIndexMask;
+    }
+
+    @Override
+    public int hashCode() {
+      int result = encoding;
+      result = 31 * result + sampleRate;
+      result = 31 * result + channelMask;
+      result = 31 * result + channelIndexMask;
+      return result;
+    }
+  }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
index 45a5557..6408ce8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java
@@ -1,20 +1,37 @@
 package org.robolectric.shadows;
 
 import static android.media.AudioTrack.ERROR_BAD_VALUE;
+import static android.media.AudioTrack.ERROR_DEAD_OBJECT;
 import static android.media.AudioTrack.WRITE_BLOCKING;
 import static android.media.AudioTrack.WRITE_NON_BLOCKING;
 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.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 com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 import android.annotation.NonNull;
+import android.media.AudioAttributes;
 import android.media.AudioFormat;
 import android.media.AudioTrack;
 import android.media.AudioTrack.WriteMode;
+import android.media.PlaybackParams;
+import android.os.Build.VERSION;
+import android.os.Parcel;
 import android.util.Log;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
 import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -50,11 +67,24 @@
 
   protected static final int DEFAULT_MIN_BUFFER_SIZE = 1024;
 
+  // Copied from native code
+  // https://cs.android.com/android/platform/superproject/+/android13-release:frameworks/base/core/jni/android_media_AudioTrack.cpp?q=AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED
+  private static final int AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED = -20;
+
   private static final String TAG = "ShadowAudioTrack";
-  private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+  /** Direct playback support checked from {@link #native_is_direct_output_supported}. */
+  private static final Multimap<AudioFormatInfo, AudioAttributesInfo> directSupportedFormats =
+      Multimaps.synchronizedMultimap(HashMultimap.create());
+  /** Non-PCM encodings allowed for creating an AudioTrack instance. */
+  private static final Set<Integer> allowedNonPcmEncodings =
+      Collections.synchronizedSet(new HashSet<>());
+
   private static final List<OnAudioDataWrittenListener> audioDataWrittenListeners =
       new CopyOnWriteArrayList<>();
+  private static int minBufferSize = DEFAULT_MIN_BUFFER_SIZE;
+
   private int numBytesReceived;
+  private PlaybackParams playbackParams;
   @RealObject AudioTrack audioTrack;
 
   /**
@@ -67,6 +97,61 @@
     minBufferSize = bufferSize;
   }
 
+  /**
+   * Adds support for direct playback for the pair of {@link AudioFormat} and {@link
+   * AudioAttributes} where the format encoding must be non-PCM. Calling {@link
+   * AudioTrack#isDirectPlaybackSupported(AudioFormat, AudioAttributes)} will return {@code true}
+   * for matching {@link AudioFormat} and {@link AudioAttributes}. The matching is performed against
+   * the format's {@linkplain AudioFormat#getEncoding() encoding}, {@linkplain
+   * AudioFormat#getSampleRate() sample rate}, {@linkplain AudioFormat#getChannelMask() channel
+   * mask} and {@linkplain AudioFormat#getChannelIndexMask() channel index mask}, and the
+   * attribute's {@linkplain AudioAttributes#getContentType() content type}, {@linkplain
+   * AudioAttributes#getUsage() usage} and {@linkplain AudioAttributes#getFlags() flags}.
+   *
+   * @param format The {@link AudioFormat}, which must be of a non-PCM encoding. If the encoding is
+   *     PCM, the method will throw an {@link IllegalArgumentException}.
+   * @param attr The {@link AudioAttributes}.
+   */
+  public static void addDirectPlaybackSupport(
+      @NonNull AudioFormat format, @NonNull AudioAttributes attr) {
+    checkNotNull(format);
+    checkNotNull(attr);
+    checkArgument(!isPcm(format.getEncoding()));
+
+    directSupportedFormats.put(
+        new AudioFormatInfo(
+            format.getEncoding(),
+            format.getSampleRate(),
+            format.getChannelMask(),
+            format.getChannelIndexMask()),
+        new AudioAttributesInfo(attr.getContentType(), attr.getUsage(), attr.getFlags()));
+  }
+
+  /**
+   * Clears all encodings that have been added for direct playback support with {@link
+   * #addDirectPlaybackSupport}.
+   */
+  public static void clearDirectPlaybackSupportedFormats() {
+    directSupportedFormats.clear();
+  }
+
+  /**
+   * Add a non-PCM encoding for which {@link AudioTrack} instances are allowed to be created.
+   *
+   * @param encoding One of {@link AudioFormat} {@code ENCODING_} constants that represents a
+   *     non-PCM encoding. If {@code encoding} is PCM, this method throws an {@link
+   *     IllegalArgumentException}.
+   */
+  public static void addAllowedNonPcmEncoding(int encoding) {
+    checkArgument(!isPcm(encoding));
+    allowedNonPcmEncodings.add(encoding);
+  }
+
+  /** Clears all encodings that have been added with {@link #addAllowedNonPcmEncoding(int)}. */
+  public static void clearAllowedNonPcmEncodings() {
+    allowedNonPcmEncodings.clear();
+  }
+
   @Implementation(minSdk = N, maxSdk = P)
   protected static int native_get_FCC_8() {
     // Return the value hard-coded in native code:
@@ -74,6 +159,20 @@
     return 8;
   }
 
+  @Implementation(minSdk = Q)
+  protected static boolean native_is_direct_output_supported(
+      int encoding,
+      int sampleRate,
+      int channelMask,
+      int channelIndexMask,
+      int contentType,
+      int usage,
+      int flags) {
+    return directSupportedFormats.containsEntry(
+        new AudioFormatInfo(encoding, sampleRate, channelMask, channelIndexMask),
+        new AudioAttributesInfo(contentType, usage, flags));
+  }
+
   /** Returns a predefined or default minimum buffer size. Audio format and config are neglected. */
   @Implementation
   protected static int native_get_min_buff_size(
@@ -81,24 +180,141 @@
     return minBufferSize;
   }
 
+  @Implementation(minSdk = P, maxSdk = Q)
+  protected int native_setup(
+      Object /*WeakReference<AudioTrack>*/ audioTrack,
+      Object /*AudioAttributes*/ attributes,
+      int[] sampleRate,
+      int channelMask,
+      int channelIndexMask,
+      int audioFormat,
+      int buffSizeInBytes,
+      int mode,
+      int[] sessionId,
+      long nativeAudioTrack,
+      boolean offload) {
+    // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+    if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+      return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+    }
+    return AudioTrack.SUCCESS;
+  }
+
+  @Implementation(minSdk = R, maxSdk = R)
+  protected int native_setup(
+      Object /*WeakReference<AudioTrack>*/ audioTrack,
+      Object /*AudioAttributes*/ attributes,
+      int[] sampleRate,
+      int channelMask,
+      int channelIndexMask,
+      int audioFormat,
+      int buffSizeInBytes,
+      int mode,
+      int[] sessionId,
+      long nativeAudioTrack,
+      boolean offload,
+      int encapsulationMode,
+      Object tunerConfiguration) {
+    // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+    if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+      return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+    }
+    return AudioTrack.SUCCESS;
+  }
+
+  @Implementation(minSdk = S, maxSdk = TIRAMISU)
+  protected int native_setup(
+      Object /*WeakReference<AudioTrack>*/ audioTrack,
+      Object /*AudioAttributes*/ attributes,
+      int[] sampleRate,
+      int channelMask,
+      int channelIndexMask,
+      int audioFormat,
+      int buffSizeInBytes,
+      int mode,
+      int[] sessionId,
+      long nativeAudioTrack,
+      boolean offload,
+      int encapsulationMode,
+      Object tunerConfiguration,
+      String opPackageName) {
+    // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+    if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+      return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+    }
+    return AudioTrack.SUCCESS;
+  }
+
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+  protected int native_setup(
+      Object /*WeakReference<AudioTrack>*/ audioTrack,
+      Object /*AudioAttributes*/ attributes,
+      int[] sampleRate,
+      int channelMask,
+      int channelIndexMask,
+      int audioFormat,
+      int buffSizeInBytes,
+      int mode,
+      int[] sessionId,
+      @NonNull Parcel attributionSource,
+      long nativeAudioTrack,
+      boolean offload,
+      int encapsulationMode,
+      Object tunerConfiguration,
+      @NonNull String opPackageName) {
+    // If offload, AudioTrack.Builder.build() has checked offload support via AudioSystem.
+    if (!offload && !isPcm(audioFormat) && !allowedNonPcmEncodings.contains(audioFormat)) {
+      return AUDIOTRACK_ERROR_SETUP_NATIVEINITFAILED;
+    }
+    return AudioTrack.SUCCESS;
+  }
+
   /**
-   * Always return the number of bytes to write. This method returns immedidately even with {@link
-   * AudioTrack#WRITE_BLOCKING}
+   * Returns the number of bytes to write. This method returns immediately even with {@link
+   * AudioTrack#WRITE_BLOCKING}. If the {@link AudioTrack} instance was created with a non-PCM
+   * encoding and the encoding can no longer be played directly, the method will return {@link
+   * AudioTrack#ERROR_DEAD_OBJECT};
    */
   @Implementation(minSdk = M)
   protected final int native_write_byte(
       byte[] audioData, int offsetInBytes, int sizeInBytes, int format, boolean isBlocking) {
+    int encoding = audioTrack.getAudioFormat();
+    // Assume that offload support does not change during the lifetime of the instance.
+    if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+        && !isPcm(encoding)
+        && !allowedNonPcmEncodings.contains(encoding)) {
+      return ERROR_DEAD_OBJECT;
+    }
     return sizeInBytes;
   }
 
+  @Implementation(minSdk = M)
+  public void setPlaybackParams(@NonNull PlaybackParams params) {
+    playbackParams = checkNotNull(params, "Illegal null params");
+  }
+
+  @Implementation(minSdk = M)
+  @NonNull
+  protected final PlaybackParams getPlaybackParams() {
+    return playbackParams;
+  }
+
   /**
-   * Always return the number of bytes to write except with invalid parameters. Assumes AudioTrack
-   * is already initialized (object properly created). Do not block even if AudioTrack in offload
-   * mode is in STOPPING play state. This method returns immediately even with {@link
-   * AudioTrack#WRITE_BLOCKING}
+   * Returns the number of bytes to write, except with invalid parameters. If the {@link AudioTrack}
+   * was created for a non-PCM encoding that can no longer be played directly, it returns {@link
+   * AudioTrack#ERROR_DEAD_OBJECT}. Assumes {@link AudioTrack} is already initialized (object
+   * properly created). Do not block even if {@link AudioTrack} in offload mode is in STOPPING play
+   * state. This method returns immediately even with {@link AudioTrack#WRITE_BLOCKING}
    */
   @Implementation(minSdk = LOLLIPOP)
   protected int write(@NonNull ByteBuffer audioData, int sizeInBytes, @WriteMode int writeMode) {
+    int encoding = audioTrack.getAudioFormat();
+    // Assume that offload support does not change during the lifetime of the instance.
+    if ((VERSION.SDK_INT < 29 || !audioTrack.isOffloadedPlayback())
+        && !isPcm(encoding)
+        && !allowedNonPcmEncodings.contains(encoding)) {
+      return ERROR_DEAD_OBJECT;
+    }
     if (writeMode != WRITE_BLOCKING && writeMode != WRITE_NON_BLOCKING) {
       Log.e(TAG, "ShadowAudioTrack.write() called with invalid blocking mode");
       return ERROR_BAD_VALUE;
@@ -150,5 +366,103 @@
   @Resetter
   public static void resetTest() {
     audioDataWrittenListeners.clear();
+    clearDirectPlaybackSupportedFormats();
+    clearAllowedNonPcmEncodings();
+  }
+
+  private static boolean isPcm(int encoding) {
+    switch (encoding) {
+      case AudioFormat.ENCODING_PCM_8BIT:
+      case AudioFormat.ENCODING_PCM_16BIT:
+      case AudioFormat.ENCODING_PCM_24BIT_PACKED:
+      case AudioFormat.ENCODING_PCM_32BIT:
+      case AudioFormat.ENCODING_PCM_FLOAT:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  /**
+   * Specific fields from {@link AudioFormat} that are used for detection of direct playback
+   * support.
+   *
+   * @see #native_is_direct_output_supported
+   */
+  private static class AudioFormatInfo {
+    private final int encoding;
+    private final int sampleRate;
+    private final int channelMask;
+    private final int channelIndexMask;
+
+    public AudioFormatInfo(int encoding, int sampleRate, int channelMask, int channelIndexMask) {
+      this.encoding = encoding;
+      this.sampleRate = sampleRate;
+      this.channelMask = channelMask;
+      this.channelIndexMask = channelIndexMask;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof AudioFormatInfo)) {
+        return false;
+      }
+
+      AudioFormatInfo other = (AudioFormatInfo) o;
+      return encoding == other.encoding
+          && sampleRate == other.sampleRate
+          && channelMask == other.channelMask
+          && channelIndexMask == other.channelIndexMask;
+    }
+
+    @Override
+    public int hashCode() {
+      int result = encoding;
+      result = 31 * result + sampleRate;
+      result = 31 * result + channelMask;
+      result = 31 * result + channelIndexMask;
+      return result;
+    }
+  }
+
+  /**
+   * Specific fields from {@link AudioAttributes} used for detection of direct playback support.
+   *
+   * @see #native_is_direct_output_supported
+   */
+  private static class AudioAttributesInfo {
+    private final int contentType;
+    private final int usage;
+    private final int flags;
+
+    public AudioAttributesInfo(int contentType, int usage, int flags) {
+      this.contentType = contentType;
+      this.usage = usage;
+      this.flags = flags;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof AudioAttributesInfo)) {
+        return false;
+      }
+
+      AudioAttributesInfo other = (AudioAttributesInfo) o;
+      return contentType == other.contentType && usage == other.usage && flags == other.flags;
+    }
+
+    @Override
+    public int hashCode() {
+      int result = contentType;
+      result = 31 * result + usage;
+      result = 31 * result + flags;
+      return result;
+    }
   }
 }
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 737df1f..7b6ff7e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothGatt.java
@@ -18,6 +18,8 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
+import javax.annotation.Nullable;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -40,8 +42,12 @@
   private boolean isClosed = false;
   private byte[] writtenBytes;
   private byte[] readBytes;
+  // TODO: ShadowBluetoothGatt.services should be removed in favor of just using the real
+  // BluetoothGatt.mServices.
   private final Set<BluetoothGattService> discoverableServices = new HashSet<>();
   private final ArrayList<BluetoothGattService> services = new ArrayList<>();
+  private final Set<BluetoothGattCharacteristic> characteristicNotificationEnableSet =
+      new HashSet<>();
 
   @RealObject private BluetoothGatt realBluetoothGatt;
   @ReflectorObject protected BluetoothGattReflector bluetoothGattReflector;
@@ -185,6 +191,7 @@
   protected boolean discoverServices() {
     this.services.clear();
     if (!this.discoverableServices.isEmpty()) {
+      // TODO: Don't store the services in the shadow.
       this.services.addAll(this.discoverableServices);
 
       if (this.getGattCallback() != null) {
@@ -204,10 +211,39 @@
    */
   @Implementation(minSdk = O)
   protected List<BluetoothGattService> getServices() {
+    // TODO: Remove this method when real BluetoothGatt#getServices() works.
     return new ArrayList<>(this.services);
   }
 
   /**
+   * Overrides {@link BluetoothGatt#getService} to return a service with given UUID.
+   *
+   * @return a service with given UUID that have been discovered through {@link
+   *     ShadowBluetoothGatt#discoverServices}.
+   */
+  @Implementation(minSdk = O)
+  @Nullable
+  protected BluetoothGattService getService(UUID uuid) {
+    // TODO: Remove this method when real BluetoothGatt#getService() works.
+    for (BluetoothGattService service : this.services) {
+      if (service.getUuid().equals(uuid)) {
+        return service;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Overrides {@link BluetoothGatt#setCharacteristicNotification} so it returns true (false) if
+   * allowCharacteristicNotification (disallowCharacteristicNotification) is called.
+   */
+  @Implementation(minSdk = O)
+  protected boolean setCharacteristicNotification(
+      BluetoothGattCharacteristic characteristic, boolean enable) {
+    return characteristicNotificationEnableSet.contains(characteristic) == enable;
+  }
+
+  /**
    * Reads bytes from incoming characteristic if properties are valid and callback is set. Callback
    * responds with {@link BluetoothGattCallback#onCharacteristicWrite} and returns true when
    * successful.
@@ -258,6 +294,16 @@
     return true;
   }
 
+  /** Allows the incoming characteristic to be set to enable notification. */
+  public void allowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+    characteristicNotificationEnableSet.add(characteristic);
+  }
+
+  /** Disallows the incoming characteristic to be set to enable notification. */
+  public void disallowCharacteristicNotification(BluetoothGattCharacteristic characteristic) {
+    characteristicNotificationEnableSet.remove(characteristic);
+  }
+
   public void addDiscoverableService(BluetoothGattService service) {
     this.discoverableServices.add(service);
   }
@@ -294,6 +340,49 @@
     return this.readBytes;
   }
 
+  public BluetoothConnectionManager getBluetoothConnectionManager() {
+    return BluetoothConnectionManager.getInstance();
+  }
+
+  /**
+   * Simulate a successful Gatt Client Conection with {@link BluetoothConnectionManager}. Performs a
+   * {@link BluetoothGattCallback#onConnectionStateChange} if available.
+   *
+   * @param remoteAddress address of Gatt client
+   */
+  public void notifyConnection(String remoteAddress) {
+    BluetoothConnectionManager.getInstance().registerGattClientConnection(remoteAddress);
+    this.isConnected = true;
+    if (this.isCallbackAppropriate()) {
+      this.getGattCallback()
+          .onConnectionStateChange(
+              this.realBluetoothGatt, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED);
+    }
+  }
+
+  /**
+   * Simulate a successful Gatt Client Disconnection with {@link BluetoothConnectionManager}.
+   * Performs a {@link BluetoothGattCallback#onConnectionStateChange} if available.
+   *
+   * @param remoteAddress address of Gatt client
+   */
+  public void notifyDisconnection(String remoteAddress) {
+    BluetoothConnectionManager.getInstance().unregisterGattClientConnection(remoteAddress);
+    if (this.isCallbackAppropriate()) {
+      this.getGattCallback()
+          .onConnectionStateChange(
+              this.realBluetoothGatt,
+              BluetoothGatt.GATT_SUCCESS,
+              BluetoothProfile.STATE_DISCONNECTED);
+    }
+    this.isConnected = false;
+  }
+
+  private boolean isCallbackAppropriate() {
+    return this.getGattCallback() != null && this.isConnected;
+  }
+
+
   @ForType(BluetoothGatt.class)
   private interface BluetoothGattReflector {
 
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 54b9626..f139288 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBluetoothHeadset.java
@@ -3,13 +3,17 @@
 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 java.util.stream.Collectors.toCollection;
 
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Objects;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
@@ -21,7 +25,8 @@
 @NotThreadSafe
 @Implements(value = BluetoothHeadset.class)
 public class ShadowBluetoothHeadset {
-  private final List<BluetoothDevice> connectedDevices = new ArrayList<>();
+
+  private final Map<BluetoothDevice, Integer> bluetoothDevices = new HashMap<>();
   private boolean allowsSendVendorSpecificResultCode = true;
   private BluetoothDevice activeBluetoothDevice;
   private boolean isVoiceRecognitionSupported = true;
@@ -32,12 +37,29 @@
    */
   @Implementation
   protected List<BluetoothDevice> getConnectedDevices() {
-    return connectedDevices;
+    return bluetoothDevices.entrySet().stream()
+        .filter(entry -> entry.getValue() == BluetoothProfile.STATE_CONNECTED)
+        .map(Entry::getKey)
+        .collect(toCollection(ArrayList::new));
   }
 
   /** Adds the given BluetoothDevice to the shadow's list of "connected devices" */
   public void addConnectedDevice(BluetoothDevice device) {
-    connectedDevices.add(device);
+    addDevice(device, BluetoothProfile.STATE_CONNECTED);
+  }
+
+  /**
+   * Adds the provided BluetoothDevice to the shadow profile's device list with an associated
+   * connectionState. The provided connection state will be returned by {@link
+   * ShadowBluetoothHeadset#getConnectionState}.
+   */
+  public void addDevice(BluetoothDevice bluetoothDevice, int connectionState) {
+    bluetoothDevices.put(bluetoothDevice, connectionState);
+  }
+
+  /** Remove the given BluetoothDevice from the shadow profile's device list */
+  public void removeDevice(BluetoothDevice bluetoothDevice) {
+    bluetoothDevices.remove(bluetoothDevice);
   }
 
   /**
@@ -49,9 +71,7 @@
    */
   @Implementation
   protected int getConnectionState(BluetoothDevice device) {
-    return connectedDevices.contains(device)
-        ? BluetoothProfile.STATE_CONNECTED
-        : BluetoothProfile.STATE_DISCONNECTED;
+    return bluetoothDevices.getOrDefault(device, BluetoothProfile.STATE_DISCONNECTED);
   }
 
   /**
@@ -63,7 +83,7 @@
    */
   @Implementation
   protected boolean startVoiceRecognition(BluetoothDevice bluetoothDevice) {
-    if (bluetoothDevice == null || !connectedDevices.contains(bluetoothDevice)) {
+    if (bluetoothDevice == null || !getConnectedDevices().contains(bluetoothDevice)) {
       return false;
     }
     if (activeBluetoothDevice != null) {
@@ -113,7 +133,7 @@
     if (command == null) {
       throw new IllegalArgumentException("Command cannot be null");
     }
-    return allowsSendVendorSpecificResultCode && connectedDevices.contains(device);
+    return allowsSendVendorSpecificResultCode && getConnectedDevices().contains(device);
   }
 
   @Nullable
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
index b0cc137..c1c887a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowBuild.java
@@ -23,6 +23,12 @@
   private static String serialOverride = Build.UNKNOWN;
 
   /**
+   * Temporary constant for VERSION_CODES.UPSIDE_DOWN_CAKE. Will be removed and replaced once the
+   * constant is available upstream.
+   */
+  public static final int UPSIDE_DOWN_CAKE = 34;
+
+  /**
    * Sets the value of the {@link Build#DEVICE} field.
    *
    * <p>It will be reset for the next test.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
index 55b9b68..19be93a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowCameraManager.java
@@ -77,7 +77,18 @@
     cameraTorches.put(cameraId, enabled);
   }
 
-  @Implementation(minSdk = Build.VERSION_CODES.S)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+  protected CameraDevice openCameraDeviceUserAsync(
+      String cameraId,
+      CameraDevice.StateCallback callback,
+      Executor executor,
+      final int uid,
+      final int oomScoreOffset,
+      boolean overrideToPortrait) {
+    return openCameraDeviceUserAsync(cameraId, callback, executor, uid, oomScoreOffset);
+  }
+
+  @Implementation(minSdk = Build.VERSION_CODES.S, maxSdk = Build.VERSION_CODES.TIRAMISU)
   protected CameraDevice openCameraDeviceUserAsync(
       String cameraId,
       CameraDevice.StateCallback callback,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
index cf1aac2..a4b1fb7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowChoreographer.java
@@ -54,13 +54,13 @@
    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
    */
   public static void setFrameDelay(Duration delay) {
-    checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
     frameDelay = delay;
   }
 
   /** See {@link #setFrameDelay(Duration)}. */
   public static Duration getFrameDelay() {
-    checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
     return frameDelay;
   }
 
@@ -72,13 +72,13 @@
    * <p>Only works in {@link LooperMode.Mode#PAUSED} looper mode.
    */
   public static void setPaused(boolean paused) {
-    checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
     isPaused = paused;
   }
 
   /** See {@link #setPaused(boolean)}. */
   public static boolean isPaused() {
-    checkState(ShadowLooper.looperMode().equals(Mode.PAUSED), "Looper must be %s", Mode.PAUSED);
+    checkState(!ShadowLooper.looperMode().equals(Mode.LEGACY), "Looper cannot be %s", Mode.LEGACY);
     return isPaused;
   }
 
@@ -109,11 +109,11 @@
    */
   @Deprecated
   public static void setPostFrameCallbackDelay(int delayMillis) {
-    if (looperMode() == Mode.PAUSED) {
+    if (looperMode() == Mode.LEGACY) {
+      ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
+    } else {
       setPaused(delayMillis != 0);
       setFrameDelay(Duration.ofMillis(delayMillis == 0 ? 1 : delayMillis));
-    } else {
-      ShadowLegacyChoreographer.setPostFrameCallbackDelay(delayMillis);
     }
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
index 9330246..75566ce 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayEventReceiver.java
@@ -12,6 +12,7 @@
 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.os.MessageQueue;
 import android.os.SystemClock;
@@ -29,9 +30,8 @@
 import org.robolectric.annotation.ReflectorObject;
 import org.robolectric.res.android.NativeObjRegistry;
 import org.robolectric.shadow.api.Shadow;
-import org.robolectric.util.ReflectionHelpers;
-import org.robolectric.util.ReflectionHelpers.ClassParameter;
 import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.Constructor;
 import org.robolectric.util.reflector.Direct;
 import org.robolectric.util.reflector.ForType;
 import org.robolectric.util.reflector.WithType;
@@ -86,7 +86,7 @@
             new NativeDisplayEventReceiver(new WeakReference<>((DisplayEventReceiver) receiver)));
   }
 
-  @Implementation(minSdk = R)
+  @Implementation(minSdk = R, maxSdk = TIRAMISU)
   protected static long nativeInit(
       WeakReference<DisplayEventReceiver> receiver,
       MessageQueue msgQueue,
@@ -95,11 +95,21 @@
     return nativeInit(receiver, msgQueue);
   }
 
-  @Implementation(minSdk = KITKAT_WATCH)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+  protected static long nativeInit(
+      WeakReference<DisplayEventReceiver> receiver,
+      WeakReference<Object> vsyncEventData,
+      MessageQueue msgQueue,
+      int vsyncSource,
+      int eventRegistration,
+      long layerHandle) {
+    return nativeInit(receiver, msgQueue);
+  }
+
+  @Implementation(minSdk = KITKAT_WATCH, maxSdk = TIRAMISU)
   protected static void nativeDispose(long receiverPtr) {
     NativeDisplayEventReceiver receiver = nativeObjRegistry.unregister(receiverPtr);
     if (receiver != null) {
-      receiver.dispose();
     }
   }
 
@@ -141,24 +151,11 @@
       displayEventReceiverReflector.onVsync(
           ShadowSystem.nanoTime(), 0L, /* SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN */ 1);
     } else if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
-      try {
-        // onVsync takes a package-private VSyncData class as a parameter, thus reflection
-        // needs to be used
-        Object vsyncData =
-            ReflectionHelpers.callConstructor(
-                Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
-                ClassParameter.from(long.class, 1), /* id */
-                ClassParameter.from(long.class, 10), /* frameDeadline */
-                ClassParameter.from(long.class, 1)); /* frameInterval */
-
-        displayEventReceiverReflector.onVsync(
-            ShadowSystem.nanoTime(),
-            0L, /* physicalDisplayId currently ignored */
-            /* frame= */ 1,
-            vsyncData /* VsyncEventData */);
-      } catch (ClassNotFoundException e) {
-        throw new LinkageError("Unable to construct VsyncEventData", e);
-      }
+      displayEventReceiverReflector.onVsync(
+          ShadowSystem.nanoTime(),
+          0L, /* physicalDisplayId currently ignored */
+          /* frame= */ 1,
+          newVsyncEventData() /* VsyncEventData */);
     } else {
       displayEventReceiverReflector.onVsync(
           ShadowSystem.nanoTime(),
@@ -240,6 +237,11 @@
   }
 
   private static Object /* VsyncEventData */ newVsyncEventData() {
+    VsyncEventDataReflector vsyncEventDataReflector = reflector(VsyncEventDataReflector.class);
+    if (RuntimeEnvironment.getApiLevel() < TIRAMISU) {
+      return vsyncEventDataReflector.newVsyncEventData(
+          /* id= */ 1, /* frameDeadline= */ 10, /* frameInterval= */ 1);
+    }
     try {
       // onVsync on T takes a package-private VsyncEventData class, which is itself composed of a
       // package private VsyncEventData.FrameTimeline  class. So use reflection to build these up
@@ -247,33 +249,26 @@
           Class.forName("android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline");
 
       int timelineArrayLength = RuntimeEnvironment.getApiLevel() == TIRAMISU ? 1 : 7;
-
+      FrameTimelineReflector frameTimelineReflector = reflector(FrameTimelineReflector.class);
       Object timelineArray = Array.newInstance(frameTimelineClass, timelineArrayLength);
       for (int i = 0; i < timelineArrayLength; i++) {
-        Array.set(timelineArray, i, newFrameTimeline(frameTimelineClass));
+        Array.set(timelineArray, i, frameTimelineReflector.newFrameTimeline(1, 1, 10));
       }
-
-      // get FrameTimeline[].class
-      Class<?> frameTimeLineArrayClass =
-          Class.forName("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;");
-      return ReflectionHelpers.callConstructor(
-          Class.forName("android.view.DisplayEventReceiver$VsyncEventData"),
-          ClassParameter.from(frameTimeLineArrayClass, timelineArray),
-          ClassParameter.from(int.class, 0), /* frameDeadline */
-          ClassParameter.from(long.class, 1)); /* frameInterval */
+      if (RuntimeEnvironment.getApiLevel() <= TIRAMISU) {
+        return vsyncEventDataReflector.newVsyncEventData(
+            timelineArray, /* preferredFrameTimelineIndex= */ 0, /* frameInterval= */ 1);
+      } else {
+        return vsyncEventDataReflector.newVsyncEventData(
+            timelineArray,
+            /* preferredFrameTimelineIndex= */ 0,
+            timelineArrayLength,
+            /* frameInterval= */ 1);
+      }
     } catch (ClassNotFoundException e) {
       throw new LinkageError("Unable to construct VsyncEventData", e);
     }
   }
 
-  private static Object newFrameTimeline(Class<?> frameTimelineClass) {
-    return ReflectionHelpers.callConstructor(
-        frameTimelineClass,
-        ClassParameter.from(long.class, 1) /* vsync id */,
-        ClassParameter.from(long.class, 1) /* expectedPresentTime */,
-        ClassParameter.from(long.class, 10) /* deadline */);
-  }
-
   /** Reflector interface for {@link DisplayEventReceiver}'s internals. */
   @ForType(DisplayEventReceiver.class)
   protected interface DisplayEventReceiverReflector {
@@ -295,5 +290,35 @@
 
     @Accessor("mCloseGuard")
     CloseGuard getCloseGuard();
+
+    @Accessor("mReceiverPtr")
+    long getReceiverPtr();
+  }
+
+  @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData")
+  interface VsyncEventDataReflector {
+    @Constructor
+    Object newVsyncEventData(long id, long frameDeadline, long frameInterval);
+
+    @Constructor
+    Object newVsyncEventData(
+        @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+            Object frameTimelineArray,
+        int preferredFrameTimelineIndex,
+        long frameInterval);
+
+    @Constructor
+    Object newVsyncEventData(
+        @WithType("[Landroid.view.DisplayEventReceiver$VsyncEventData$FrameTimeline;")
+            Object frameTimelineArray,
+        int preferredFrameTimelineIndex,
+        int timelineArrayLength,
+        long frameInterval);
+  }
+
+  @ForType(className = "android.view.DisplayEventReceiver$VsyncEventData$FrameTimeline")
+  interface FrameTimelineReflector {
+    @Constructor
+    Object newFrameTimeline(long id, long expectedPresentTime, long deadline);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
index 9f79573..1aa08b0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowDisplayManagerGlobal.java
@@ -1,6 +1,5 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
 import static android.os.Build.VERSION_CODES.O_MR1;
 import static android.os.Build.VERSION_CODES.P;
@@ -28,7 +27,6 @@
 import java.util.TreeMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import javax.annotation.Nullable;
-import org.robolectric.RuntimeEnvironment;
 import org.robolectric.android.Bootstrap;
 import org.robolectric.annotation.HiddenApi;
 import org.robolectric.annotation.Implementation;
@@ -88,22 +86,28 @@
             reflector(DisplayManagerGlobalReflector.class, instance);
     displayManagerGlobal.setDm(displayManager);
     displayManagerGlobal.setLock(new Object());
-
-    List displayListeners = new CopyOnWriteArrayList();
-    try {
-      // TODO: rexhoffman when we have sufficient detection in android dev replace
-      // this with a version check.
-      Field f = DisplayManagerGlobal.class.getDeclaredField("mDisplayListeners");
-      if (f.getType().isAssignableFrom(ArrayList.class)) {
-        displayListeners = new ArrayList();
-      }
-    } catch (NoSuchFieldException e) {
-    }
+    List<Handler> displayListeners = createDisplayListeners();
     displayManagerGlobal.setDisplayListeners(displayListeners);
     displayManagerGlobal.setDisplayInfoCache(new SparseArray<>());
     return instance;
   }
 
+  private static List<Handler> createDisplayListeners() {
+    try {
+      // The type for mDisplayListeners was changed from ArrayList to CopyOnWriteArrayList
+      // in some branches of T and U, so we need to reflect on DisplayManagerGlobal class
+      // to check the type of mDisplayListeners member before initializing appropriately.
+      Field f = DisplayManagerGlobal.class.getDeclaredField("mDisplayListeners");
+      if (f.getType().isAssignableFrom(ArrayList.class)) {
+        return new ArrayList<>();
+      } else {
+        return new CopyOnWriteArrayList<>();
+      }
+    } catch (NoSuchFieldException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   @VisibleForTesting
   static DisplayManagerGlobal getGlobalInstance() {
     return instance;
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
index 791ece2..58cd558 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageDecoder.java
@@ -10,6 +10,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.ColorSpace;
+import android.graphics.ColorSpace.Named;
 import android.graphics.ImageDecoder;
 import android.graphics.ImageDecoder.DecodeException;
 import android.graphics.ImageDecoder.Source;
@@ -247,14 +248,16 @@
   static String ImageDecoder_nGetMimeType(long nativePtr) {
     CppImageDecoder decoder = NATIVE_IMAGE_DECODER_REGISTRY.getNativeObject(nativePtr);
     // return encodedFormatToString(decoder.mCodec.getEncodedFormat());
-    throw new UnsupportedOperationException();
+    // TODO: fix this properly. Just hardcode to png for now or just remove GraphicsMode.LEGACY
+    return "image/png";
   }
 
   static ColorSpace ImageDecoder_nGetColorSpace(long nativePtr) {
     // auto colorType = codec.computeOutputColorType(codec.getInfo().colorType());
     // sk_sp<SkColorSpace> colorSpace = codec.computeOutputColorSpace(colorType);
     // return GraphicsJNI.getColorSpace(colorSpace, colorType);
-    throw new UnsupportedOperationException();
+    // TODO: fix this properly. Just hardcode to SRGB for now or just remove GraphicsMode.LEGACY
+    return ColorSpace.get(Named.SRGB);
   }
 
   // native method implementations...
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
index 0654fbc..b8564ca 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowImageReader.java
@@ -1,6 +1,5 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 import static android.os.Build.VERSION_CODES.KITKAT;
 import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -70,7 +69,7 @@
     return nativeImageSetup(image);
   }
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected int nativeImageSetup(Object /* Image */ image) {
     return nativeImageSetup((Image) image);
   }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
index 50635c8..298fabe 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputManager.java
@@ -1,10 +1,14 @@
 package org.robolectric.shadows;
 
+import static android.os.Build.VERSION.SDK_INT;
 import static android.os.Build.VERSION_CODES.KITKAT;
 import static android.os.Build.VERSION_CODES.R;
+import static android.os.Build.VERSION_CODES.TIRAMISU;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.robolectric.util.reflector.Reflector.reflector;
 
 import android.hardware.input.InputManager;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.KeyEvent;
@@ -13,13 +17,18 @@
 import android.view.VerifiedMotionEvent;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
 import org.robolectric.annotation.Resetter;
 import org.robolectric.util.ReflectionHelpers;
+import org.robolectric.util.reflector.Accessor;
+import org.robolectric.util.reflector.ForType;
 
 /** Shadow for {@link InputManager} */
 @Implements(value = InputManager.class, looseSignatures = true)
 public class ShadowInputManager {
 
+  @RealObject InputManager realInputManager;
+
   @Implementation
   protected boolean injectInputEvent(InputEvent event, int mode) {
     // ignore
@@ -37,6 +46,35 @@
     return new int[0];
   }
 
+  @Implementation(maxSdk = TIRAMISU)
+  protected void populateInputDevicesLocked() throws ClassNotFoundException {
+    if (ReflectionHelpers.getField(realInputManager, "mInputDevicesChangedListener") == null) {
+      ReflectionHelpers.setField(
+          realInputManager,
+          "mInputDevicesChangedListener",
+          ReflectionHelpers.callConstructor(
+              Class.forName("android.hardware.input.InputManager$InputDevicesChangedListener")));
+    }
+
+    if (getInputDevices() == null) {
+      final int[] ids = realInputManager.getInputDeviceIds();
+
+      SparseArray<InputDevice> inputDevices = new SparseArray<>();
+      for (int i = 0; i < ids.length; i++) {
+        inputDevices.put(ids[i], null);
+      }
+      setInputDevices(inputDevices);
+    }
+  }
+
+  private SparseArray<InputDevice> getInputDevices() {
+    return reflector(InputManagerReflector.class, realInputManager).getInputDevices();
+  }
+
+  private void setInputDevices(SparseArray<InputDevice> devices) {
+    reflector(InputManagerReflector.class, realInputManager).setInputDevices(devices);
+  }
+
   /**
    * Provides a local java implementation, since the real implementation is in system server +
    * native code.
@@ -78,6 +116,17 @@
 
   @Resetter
   public static void reset() {
-    ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+    if (SDK_INT < ShadowBuild.UPSIDE_DOWN_CAKE) {
+      ReflectionHelpers.setStaticField(InputManager.class, "sInstance", null);
+    }
+  }
+
+  @ForType(InputManager.class)
+  interface InputManagerReflector {
+    @Accessor("mInputDevices")
+    SparseArray<InputDevice> getInputDevices();
+
+    @Accessor("mInputDevices")
+    void setInputDevices(SparseArray<InputDevice> devices);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
index 42baa4c..bf87b3a 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowJobService.java
@@ -2,6 +2,7 @@
 
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 
+import android.app.Notification;
 import android.app.job.JobParameters;
 import android.app.job.JobService;
 import org.robolectric.annotation.Implementation;
@@ -19,6 +20,14 @@
     this.isRescheduleNeeded = needsReschedule;
   }
 
+  /** Stubbed out for now, as the real implementation throws an NPE when executed in Robolectric. */
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+  protected void setNotification(
+      JobParameters params,
+      int notificationId,
+      Notification notification,
+      int jobEndNotificationPolicy) {}
+
   /**
    * Returns whether the job has finished running. When using this shadow this returns true after
    * {@link #jobFinished(JobParameters, boolean)} is called.
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
index 4b28baf..e06f348 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLauncherApps.java
@@ -49,6 +49,7 @@
 public class ShadowLauncherApps {
   private List<ShortcutInfo> shortcuts = new ArrayList<>();
   private final Multimap<UserHandle, String> enabledPackages = HashMultimap.create();
+  private final Multimap<UserHandle, ComponentName> enabledActivities = HashMultimap.create();
   private final Multimap<UserHandle, LauncherActivityInfo> shortcutActivityList =
       HashMultimap.create();
   private final Multimap<UserHandle, LauncherActivityInfo> activityList = HashMultimap.create();
@@ -100,6 +101,17 @@
   }
 
   /**
+   * Sets an activity referenced by ComponentName as enabled, to be checked by {@link
+   * #isActivityEnabled(ComponentName, UserHandle)}.
+   *
+   * @param userHandle the user handle to be set.
+   * @param componentName the component name of the activity to be enabled.
+   */
+  public void setActivityEnabled(UserHandle userHandle, ComponentName componentName) {
+    enabledActivities.put(userHandle, componentName);
+  }
+
+  /**
    * Adds a {@link LauncherActivityInfo} to be retrieved by {@link
    * #getShortcutConfigActivityList(String, UserHandle)}.
    *
@@ -219,10 +231,9 @@
         "This method is not currently supported in Robolectric.");
   }
 
-  @Implementation
+  @Implementation(minSdk = L)
   protected boolean isActivityEnabled(ComponentName component, UserHandle user) {
-    throw new UnsupportedOperationException(
-        "This method is not currently supported in Robolectric.");
+    return enabledActivities.containsEntry(user, component);
   }
 
   /**
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
index f624b60..2fb348e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLegacyLooper.java
@@ -58,7 +58,7 @@
   @Resetter
   public static synchronized void resetThreadLoopers() {
     // do not use looperMode() here, because its cached value might already have been reset
-    if (ConfigurationRegistry.get(LooperMode.Mode.class) == LooperMode.Mode.PAUSED) {
+    if (ConfigurationRegistry.get(LooperMode.Mode.class) != LooperMode.Mode.LEGACY) {
       // ignore if realistic looper
       return;
     }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
index 2a3a61b..f954ac2 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLoadedApk.java
@@ -1,20 +1,38 @@
 package org.robolectric.shadows;
 
+import static org.robolectric.shadow.api.Shadow.newInstanceOf;
+import static org.robolectric.util.reflector.Reflector.reflector;
+
 import android.app.Application;
 import android.app.LoadedApk;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.os.Build.VERSION_CODES;
+import org.robolectric.RuntimeEnvironment;
 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(value = LoadedApk.class, isInAndroidSdk = false)
 public class ShadowLoadedApk {
+  @RealObject private LoadedApk realLoadedApk;
+  private boolean isClassLoaderInitialized = false;
+  private final Object classLoaderLock = new Object();
 
   @Implementation
   public ClassLoader getClassLoader() {
+    // The AppComponentFactory was introduced from SDK 28.
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+      synchronized (classLoaderLock) {
+        if (!isClassLoaderInitialized) {
+          isClassLoaderInitialized = true;
+          tryInitAppComponentFactory(realLoadedApk);
+        }
+      }
+    }
     return this.getClass().getClassLoader();
   }
 
@@ -23,6 +41,35 @@
     return this.getClass().getClassLoader();
   }
 
+  private void tryInitAppComponentFactory(LoadedApk realLoadedApk) {
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.P) {
+      ApplicationInfo applicationInfo = realLoadedApk.getApplicationInfo();
+      if (applicationInfo == null || applicationInfo.appComponentFactory == null) {
+        return;
+      }
+      _LoadedApk_ loadedApkReflector = reflector(_LoadedApk_.class, realLoadedApk);
+      if (!loadedApkReflector.getIncludeCode()) {
+        return;
+      }
+      String fullQualifiedClassName =
+          calculateFullQualifiedClassName(
+              applicationInfo.appComponentFactory, applicationInfo.packageName);
+      android.app.AppComponentFactory factory =
+          (android.app.AppComponentFactory) newInstanceOf(fullQualifiedClassName);
+      if (factory == null) {
+        factory = new android.app.AppComponentFactory();
+      }
+      loadedApkReflector.setAppFactory(factory);
+    }
+  }
+
+  private String calculateFullQualifiedClassName(String className, String packageName) {
+    if (packageName == null) {
+      return className;
+    }
+    return className.startsWith(".") ? packageName + className : className;
+  }
+
   /** Accessor interface for {@link LoadedApk}'s internals. */
   @ForType(LoadedApk.class)
   public interface _LoadedApk_ {
@@ -32,5 +79,11 @@
 
     @Accessor("mResources")
     void setResources(Resources resources);
+
+    @Accessor("mIncludeCode")
+    boolean getIncludeCode();
+
+    @Accessor("mAppComponentFactory")
+    void setAppFactory(Object appFactory);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
index 103907b..9bef2d1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaCodec.java
@@ -1,6 +1,5 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 import static android.os.Build.VERSION_CODES.JELLY_BEAN;
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static android.os.Build.VERSION_CODES.N_MR1;
@@ -409,7 +408,7 @@
   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
   protected void invalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void invalidateByteBufferLocked(
       @Nullable ByteBuffer[] buffers, int index, boolean input) {}
 
@@ -417,14 +416,14 @@
   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
   protected void validateInputByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void validateInputByteBufferLocked(@Nullable ByteBuffer[] buffers, int index) {}
 
   /** Prevents calling Android-only methods on basic ByteBuffer objects. */
   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
   protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index) {}
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void revalidateByteBuffer(@Nullable ByteBuffer[] buffers, int index, boolean input) {}
 
   /**
@@ -442,7 +441,7 @@
     }
   }
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void validateOutputByteBufferLocked(
       @Nullable ByteBuffer[] buffers, int index, @NonNull BufferInfo info) {
     validateOutputByteBuffer(buffers, index, info);
@@ -452,14 +451,14 @@
   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
   protected void invalidateByteBuffers(@Nullable ByteBuffer[] buffers) {}
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void invalidateByteBuffersLocked(@Nullable ByteBuffer[] buffers) {}
 
   /** Prevents attempting to free non-direct ByteBuffer objects. */
   @Implementation(minSdk = LOLLIPOP, maxSdk = TIRAMISU)
   protected void freeByteBuffer(@Nullable ByteBuffer buffer) {}
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected void freeByteBufferLocked(@Nullable ByteBuffer buffer) {}
 
   /** Shadows CodecBuffer to prevent attempting to free non-direct ByteBuffer objects. */
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
index 5873560..f08a53c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowMediaPlayer.java
@@ -37,6 +37,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Random;
 import java.util.TreeMap;
 import org.robolectric.annotation.Implementation;
@@ -112,8 +113,7 @@
   private static final Map<DataSource, Exception> exceptions = new HashMap<>();
   private static final Map<DataSource, MediaInfo> mediaInfoMap = new HashMap<>();
 
-  private static final MediaInfoProvider DEFAULT_MEDIA_INFO_PROVIDER = mediaInfoMap::get;
-  private static MediaInfoProvider mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+  private static Optional<MediaInfoProvider> mediaInfoProvider = Optional.empty();
 
   @RealObject private MediaPlayer player;
 
@@ -650,7 +650,7 @@
    * @see #setDataSource(DataSource)
    */
   public void doSetDataSource(DataSource dataSource) {
-    MediaInfo mediaInfo = mediaInfoProvider.get(dataSource);
+    MediaInfo mediaInfo = getMediaInfo(dataSource);
     if (mediaInfo == null) {
       throw new IllegalArgumentException(
           "Don't know what to do with dataSource "
@@ -663,17 +663,16 @@
   }
 
   public static MediaInfo getMediaInfo(DataSource dataSource) {
-    return mediaInfoProvider.get(dataSource);
+    if (mediaInfoMap.containsKey(dataSource)) {
+      return mediaInfoMap.get(dataSource);
+    }
+    return mediaInfoProvider.map(provider -> provider.get(dataSource)).orElse(null);
   }
 
   /**
    * Adds a {@link MediaInfo} for a {@link DataSource}.
-   *
-   * <p>This overrides any {@link MediaInfoProvider} previously set by calling {@link
-   * #setMediaInfoProvider}, i.e., the provider will not be used for any {@link DataSource}.
    */
   public static void addMediaInfo(DataSource dataSource, MediaInfo info) {
-    ShadowMediaPlayer.mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
     mediaInfoMap.put(dataSource, info);
   }
 
@@ -685,7 +684,7 @@
    * {@link MediaInfo} provided by this {@link MediaInfoProvider} will be used instead.
    */
   public static void setMediaInfoProvider(MediaInfoProvider mediaInfoProvider) {
-    ShadowMediaPlayer.mediaInfoProvider = mediaInfoProvider;
+    ShadowMediaPlayer.mediaInfoProvider = Optional.of(mediaInfoProvider);
   }
 
   public static void addException(DataSource dataSource, RuntimeException e) {
@@ -1536,7 +1535,7 @@
   @Resetter
   public static void resetStaticState() {
     createListener = null;
-    mediaInfoProvider = DEFAULT_MEDIA_INFO_PROVIDER;
+    mediaInfoProvider = Optional.empty();
     exceptions.clear();
     mediaInfoMap.clear();
     DataSource.reset();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
index 8b7b8fa..7b045e8 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativeFontsFontFamily.java
@@ -1,6 +1,5 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 import static android.os.Build.VERSION_CODES.Q;
 import static android.os.Build.VERSION_CODES.S;
 import static android.os.Build.VERSION_CODES.TIRAMISU;
@@ -64,6 +63,12 @@
       return FontFamilyBuilderNatives.nBuild(builderPtr, langTags, variant, isCustomFallback);
     }
 
+    @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
+    protected static long nBuild(
+        long builderPtr, String langTags, int variant, boolean isCustomFallback, boolean isDefaultFallback) {
+      return nBuild(builderPtr, langTags, variant, isCustomFallback);
+    }
+
     @Implementation
     protected static long nGetReleaseNativeFamily() {
       return FontFamilyBuilderNatives.nGetReleaseNativeFamily();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
index 94fadb5..32d4280 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNativePaint.java
@@ -813,7 +813,7 @@
         paintPtr, text, start, count, ctxStart, ctxCount, isRtl, outMetrics);
   }
 
-  @Implementation(minSdk = 10000)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected static float nGetRunCharacterAdvance(
       long paintPtr,
       char[] text,
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
index 0f9e44d..4cdfb45 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowNfcAdapter.java
@@ -221,7 +221,7 @@
     }
     if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.Q) {
       nfcAdapterReflector.setHasNfcFeature(false);
-      if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.CUR_DEVELOPMENT) {
+      if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.TIRAMISU) {
         nfcAdapterReflector.setHasBeamFeature(false);
       }
     }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
index 8fec646..16a7398 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPaint.java
@@ -112,6 +112,15 @@
   }
 
   @Implementation
+  protected void setStrikeThruText(boolean strikeThruText) {
+    if (strikeThruText) {
+      setFlags(flags | Paint.STRIKE_THRU_TEXT_FLAG);
+    } else {
+      setFlags(flags & ~Paint.STRIKE_THRU_TEXT_FLAG);
+    }
+  }
+
+  @Implementation
   protected Shader setShader(Shader shader) {
     this.shader = shader;
     return shader;
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 50f8adf..ee3bef0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedLooper.java
@@ -12,6 +12,7 @@
 import android.os.MessageQueue.IdleHandler;
 import android.os.SystemClock;
 import android.util.Log;
+import com.google.common.base.Preconditions;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -58,6 +59,8 @@
   private static Set<Looper> loopingLoopers =
       Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<Looper, Boolean>()));
 
+  private static boolean ignoreUncaughtExceptions = false;
+
   @RealObject private Looper realLooper;
   private boolean isPaused = false;
   // the Executor that executes looper messages. Must be written to on looper thread
@@ -317,6 +320,51 @@
   }
 
   /**
+   * By default Robolectric will put Loopers that throw uncaught exceptions in their loop method
+   * into an error state, where any future posting to the looper's queue will throw an error.
+   *
+   * <p>This API allows you to disable this behavior. Note this is a permanent setting - it is not
+   * reset between tests.
+   *
+   * @deprecated this method only exists to accommodate legacy tests with preexisting issues.
+   *     Silently discarding exceptions is not recommended, and can lead to deadlocks.
+   */
+  @Deprecated
+  public static void setIgnoreUncaughtExceptions(boolean shouldIgnore) {
+    ignoreUncaughtExceptions = shouldIgnore;
+  }
+
+  /**
+   * Shadow loop to handle uncaught exceptions. Without this logic an uncaught exception on a looper
+   * thread will cause idle() to deadlock.
+   */
+  @Implementation
+  protected static void loop() {
+    try {
+      reflector(LooperReflector.class).loop();
+    } catch (Exception e) {
+      Looper realLooper = Preconditions.checkNotNull(Looper.myLooper());
+      ShadowPausedMessageQueue shadowQueue = Shadow.extract(realLooper.getQueue());
+
+      if (ignoreUncaughtExceptions) {
+        // ignore
+      } else {
+        shadowQueue.setUncaughtException(e);
+        // release any ControlRunnables currently in queue to prevent deadlocks
+        shadowQueue.drainQueue(
+            input -> {
+              if (input instanceof ControlRunnable) {
+                ((ControlRunnable) input).runLatch.countDown();
+                return true;
+              }
+              return false;
+            });
+      }
+      throw e;
+    }
+  }
+
+  /**
    * If the given {@code lastMessageRead} is not null and the queue is now idle, get the idle
    * handlers and run them. This synchronization mirrors what happens in the real message queue
    * next() method, but does not block after running the idle handlers.
@@ -345,21 +393,40 @@
   private abstract static class ControlRunnable implements Runnable {
 
     protected final CountDownLatch runLatch = new CountDownLatch(1);
+    private volatile RuntimeException exception;
 
-    public void waitTillComplete() {
+    @Override
+    public void run() {
+      try {
+        doRun();
+      } catch (RuntimeException e) {
+        if (!ignoreUncaughtExceptions) {
+          exception = e;
+        }
+        throw e;
+      } finally {
+        runLatch.countDown();
+      }
+    }
+
+    protected abstract void doRun() throws RuntimeException;
+
+    public void waitTillComplete() throws RuntimeException {
       try {
         runLatch.await();
       } catch (InterruptedException e) {
         Log.w("ShadowPausedLooper", "wait till idle interrupted");
       }
+      if (exception != null) {
+        throw exception;
+      }
     }
   }
 
   private class IdlingRunnable extends ControlRunnable {
 
     @Override
-    public void run() {
-      try {
+    public void doRun() {
         while (true) {
           Message msg = getNextExecutableMessage();
           if (msg == null) {
@@ -369,26 +436,20 @@
           shadowMsg(msg).recycleUnchecked();
           triggerIdleHandlersIfNeeded(msg);
         }
-      } finally {
-        runLatch.countDown();
-      }
     }
   }
 
   private class RunOneRunnable extends ControlRunnable {
 
     @Override
-    public void run() {
-      try {
+    public void doRun() {
+
         Message msg = shadowQueue().getNextIgnoringWhen();
         if (msg != null) {
           SystemClock.setCurrentTimeMillis(shadowMsg(msg).getWhen());
           msg.getTarget().dispatchMessage(msg);
           triggerIdleHandlersIfNeeded(msg);
         }
-      } finally {
-        runLatch.countDown();
-      }
     }
   }
 
@@ -408,6 +469,8 @@
       }
       looperExecutor.execute(runnable);
       runnable.waitTillComplete();
+      // throw immediately if looper died while executing tasks
+      shadowQueue().checkQueueState();
     }
   }
 
@@ -422,6 +485,7 @@
 
     @Override
     public void execute(Runnable runnable) {
+      shadowQueue().checkQueueState();
       executionQueue.add(runnable);
     }
 
@@ -435,18 +499,22 @@
           Runnable runnable = executionQueue.take();
           runnable.run();
         } catch (InterruptedException e) {
-          // ignore
+          // ignored
         }
       }
     }
+
+    @Override
+    protected void doRun() throws RuntimeException {
+      throw new UnsupportedOperationException();
+    }
   }
 
   private class UnPauseRunnable extends ControlRunnable {
     @Override
-    public void run() {
+    public void doRun() {
       setLooperExecutor(new HandlerExecutor(new Handler(realLooper)));
       isPaused = false;
-      runLatch.countDown();
     }
   }
 
@@ -478,5 +546,8 @@
 
     @Direct
     void quitSafely();
+
+    @Direct
+    void loop();
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
index 162330a..5caf016 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowPausedMessageQueue.java
@@ -16,6 +16,9 @@
 import android.os.MessageQueue;
 import android.os.MessageQueue.IdleHandler;
 import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import com.google.common.base.Predicate;
 import java.time.Duration;
 import java.util.ArrayList;
 import org.robolectric.RuntimeEnvironment;
@@ -47,6 +50,7 @@
       new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class);
   private boolean isPolling = false;
   private ShadowPausedSystemClock.Listener clockListener;
+  private Exception uncaughtException = null;
 
   // shadow constructor instead of nativeInit because nativeInit signature has changed across SDK
   // versions
@@ -55,7 +59,16 @@
     invokeConstructor(MessageQueue.class, realQueue, from(boolean.class, quitAllowed));
     int ptr = (int) nativeQueueRegistry.register(this);
     reflector(MessageQueueReflector.class, realQueue).setPtr(ptr);
-    clockListener = () -> nativeWake(ptr);
+    clockListener =
+        () -> {
+          synchronized (realQueue) {
+            // only wake up the Looper thread if queue is non empty to reduce contention if many
+            // Looper threads are active
+            if (getMessages() != null) {
+              nativeWake(ptr);
+            }
+          }
+        };
     ShadowPausedSystemClock.addStaticListener(clockListener);
   }
 
@@ -210,8 +223,28 @@
     return reflector(MessageQueueReflector.class, realQueue).getQuitAllowed();
   }
 
+  @VisibleForTesting
   void doEnqueueMessage(Message msg, long when) {
-    reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+    enqueueMessage(msg, when);
+  }
+
+  @Implementation
+  protected boolean enqueueMessage(Message msg, long when) {
+    synchronized (realQueue) {
+      if (uncaughtException != null) {
+        // looper thread has died
+        IllegalStateException e =
+            new IllegalStateException(
+                msg.getTarget()
+                    + " sending message to a Looper thread that has died due to an uncaught"
+                    + " exception",
+                uncaughtException);
+        Log.w("ShadowPausedMessageQueue", e);
+        msg.recycle();
+        throw e;
+      }
+      return reflector(MessageQueueReflector.class, realQueue).enqueueMessage(msg, when);
+    }
   }
 
   Message getMessages() {
@@ -283,7 +316,9 @@
         return Duration.ZERO;
       }
       while (next != null) {
-        when = shadowOfMsg(next).getWhen();
+        if (next.getTarget() != null) {
+          when = shadowOfMsg(next).getWhen();
+        }
         next = shadowOfMsg(next).internalGetNext();
       }
     }
@@ -309,7 +344,9 @@
     synchronized (realQueue) {
       Message next = getMessages();
       while (next != null) {
-        count++;
+        if (next.getTarget() != null) {
+          count++;
+        }
         next = shadowOfMsg(next).internalGetNext();
       }
     }
@@ -323,12 +360,24 @@
    */
   Message getNextIgnoringWhen() {
     synchronized (realQueue) {
-      Message head = getMessages();
-      if (head != null) {
-        Message next = shadowOfMsg(head).internalGetNext();
-        reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+      Message prev = null;
+      Message msg = getMessages();
+      // Head is blocked on synchronization barrier, find next asynchronous message.
+      if (msg != null && msg.getTarget() == null) {
+        do {
+          prev = msg;
+          msg = shadowOfMsg(msg).internalGetNext();
+        } while (msg != null && !msg.isAsynchronous());
       }
-      return head;
+      if (msg != null) {
+        Message next = shadowOfMsg(msg).internalGetNext();
+        if (prev == null) {
+          reflector(MessageQueueReflector.class, realQueue).setMessages(next);
+        } else {
+          ReflectionHelpers.setField(prev, "next", next);
+        }
+      }
+      return msg;
     }
   }
 
@@ -340,6 +389,7 @@
     msgQueue.setMessages(null);
     msgQueue.setIdleHandlers(new ArrayList<>());
     msgQueue.setNextBarrierToken(0);
+    setUncaughtException(null);
   }
 
   private static ShadowPausedMessage shadowOfMsg(Message head) {
@@ -378,10 +428,50 @@
     }
   }
 
+  /**
+   * Called when an uncaught exception occurred in this message queue's Looper thread.
+   *
+   * <p>In real android, by default an exception handler is installed which kills the entire process
+   * when an uncaught exception occurs. We don't want to do this in robolectric to isolate tests, so
+   * instead an uncaught exception puts the message queue into an error state, where any future
+   * interaction will rethrow the exception.
+   */
+  void setUncaughtException(Exception e) {
+    synchronized (realQueue) {
+      this.uncaughtException = e;
+    }
+  }
+
+  void checkQueueState() {
+    synchronized (realQueue) {
+      if (uncaughtException != null) {
+        throw new IllegalStateException(
+            "Looper thread that has died due to an uncaught exception", uncaughtException);
+      }
+    }
+  }
+
+  /**
+   * Remove all messages from queue
+   *
+   * @param msgProcessor a callback to apply to each mesg
+   */
+  void drainQueue(Predicate<Runnable> msgProcessor) {
+    synchronized (realQueue) {
+      Message msg = getMessages();
+      while (msg != null) {
+        boolean unused = msgProcessor.apply(msg.getCallback());
+        ShadowMessage shadowMsg = Shadow.extract(msg);
+        msg.recycle();
+        msg = shadowMsg.getNext();
+      }
+    }
+  }
+
   /** Accessor interface for {@link MessageQueue}'s internals. */
   @ForType(MessageQueue.class)
   private interface MessageQueueReflector {
-
+    @Direct
     boolean enqueueMessage(Message msg, long when);
 
     Message next();
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
index 839e285..cbf52ae 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
@@ -41,6 +41,7 @@
 import android.content.pm.ICrossProfileApps;
 import android.content.pm.IShortcutService;
 import android.content.rollback.IRollbackManager;
+import android.hardware.ISensorPrivacyManager;
 import android.hardware.biometrics.IAuthService;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.fingerprint.IFingerprintService;
@@ -57,6 +58,7 @@
 import android.net.INetworkPolicyManager;
 import android.net.INetworkScoreService;
 import android.net.ITetheringConnector;
+import android.net.IVpnManager;
 import android.net.nsd.INsdManager;
 import android.net.vcn.IVcnManagementService;
 import android.net.wifi.IWifiManager;
@@ -205,6 +207,8 @@
       addBinderService(Context.UWB_SERVICE, IUwbAdapter.class);
       addBinderService(Context.VCN_MANAGEMENT_SERVICE, IVcnManagementService.class);
       addBinderService(Context.TRANSLATION_MANAGER_SERVICE, ITranslationManager.class);
+      addBinderService(Context.SENSOR_PRIVACY_SERVICE, ISensorPrivacyManager.class);
+      addBinderService(Context.VPN_MANAGEMENT_SERVICE, IVpnManager.class);
     }
     if (RuntimeEnvironment.getApiLevel() >= TIRAMISU) {
       addBinderService(Context.AMBIENT_CONTEXT_SERVICE, IAmbientContextManager.class);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
index a1895ff..c40d24e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSoundPool.java
@@ -1,6 +1,5 @@
 package org.robolectric.shadows;
 
-import static android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
 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;
@@ -63,7 +62,7 @@
     return 1;
   }
 
-  @Implementation(minSdk = CUR_DEVELOPMENT)
+  @Implementation(minSdk = ShadowBuild.UPSIDE_DOWN_CAKE)
   protected int _play(
       int soundID,
       float leftVolume,
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 529aaa4..1b239d1 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSubscriptionManager.java
@@ -21,6 +21,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Executor;
 import org.robolectric.annotation.HiddenApi;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
@@ -282,6 +283,17 @@
   }
 
   /**
+   * Adds a listener to a local list of listeners. Will be triggered by {@link
+   * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
+   */
+  @Implementation(minSdk = R)
+  protected void addOnSubscriptionsChangedListener(
+      Executor executor, OnSubscriptionsChangedListener listener) {
+    listeners.add(listener);
+    listener.onSubscriptionsChanged();
+  }
+
+  /**
    * Removes a listener from a local list of listeners. Will be triggered by {@link
    * #setActiveSubscriptionInfoList} when the local list of {@link SubscriptionInfo} is updated.
    */
@@ -290,6 +302,16 @@
     listeners.remove(listener);
   }
 
+  /**
+   * Check if a listener exists in the {@link ShadowSubscriptionManager.listeners}.
+   *
+   * @param listener The listener to check.
+   * @return boolean True if the listener already added, otherwise false.
+   */
+  public boolean hasOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) {
+    return listeners.contains(listener);
+  }
+
   /** Returns subscription Ids that were set via {@link #setActiveSubscriptionInfoList}. */
   @Implementation(minSdk = LOLLIPOP_MR1)
   @HiddenApi
@@ -405,6 +427,17 @@
     return phoneNumberMap.getOrDefault(subscriptionId, "");
   }
 
+  /**
+   * Returns the phone number for the given {@code subscriptionId}, or an empty string if not
+   * available. {@code source} is ignored and will return the same as {@link #getPhoneNumber(int)}.
+   *
+   * <p>The phone number can be set by {@link #setPhoneNumber(int, String)}
+   */
+  @Implementation(minSdk = TIRAMISU)
+  protected String getPhoneNumber(int subscriptionId, int source) {
+    return getPhoneNumber(subscriptionId);
+  }
+
   /** Sets the phone number returned by {@link #getPhoneNumber(int)}. */
   public void setPhoneNumber(int subscriptionId, String phoneNumber) {
     phoneNumberMap.put(subscriptionId, phoneNumber);
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
index bc85287..63d12e6 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSurfaceControl.java
@@ -11,6 +11,7 @@
 import android.view.SurfaceSession;
 import dalvik.system.CloseGuard;
 import java.util.concurrent.atomic.AtomicInteger;
+import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.ReflectorObject;
@@ -82,6 +83,13 @@
 
   void initializeNativeObject() {
     surfaceControlReflector.setNativeObject(nativeObject.incrementAndGet());
+    if (RuntimeEnvironment.getApiLevel() >= ShadowBuild.UPSIDE_DOWN_CAKE) {
+      try {
+        surfaceControlReflector.setFreeNativeResources(() -> {});
+      } catch(Exception e) {
+        // tm branches not yet have mFreeNativeResources added while in partial U state
+      }
+    }
   }
 
   @ForType(SurfaceControl.class)
@@ -94,5 +102,8 @@
 
     @Direct
     void finalize();
+
+    @Accessor("mFreeNativeResources")
+    void setFreeNativeResources(Runnable runnable);
   }
 }
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
index a2bb38a..b901370 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystem.java
@@ -13,10 +13,10 @@
    */
   @SuppressWarnings("unused")
   public static long nanoTime() {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       return ShadowLegacySystemClock.nanoTime();
+    } else {
+      return TimeUnit.MILLISECONDS.toNanos(SystemClock.uptimeMillis());
     }
   }
 
@@ -27,10 +27,10 @@
    */
   @SuppressWarnings("unused")
   public static long currentTimeMillis() {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      return SystemClock.uptimeMillis();
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       return ShadowLegacySystemClock.currentTimeMillis();
+    } else {
+      return SystemClock.uptimeMillis();
     }
   }
 }
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 cce1990..41fbf4e 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowSystemVibrator.java
@@ -15,9 +15,9 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemVibrator;
-import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.vibrator.VibrationEffectSegment;
+import com.google.common.base.Preconditions;
 import java.util.List;
 import java.util.Optional;
 import org.robolectric.RuntimeEnvironment;
@@ -25,7 +25,8 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.util.ReflectionHelpers;
 
-@Implements(value = SystemVibrator.class, isInAndroidSdk = false)
+/** Shadow for {@link SystemVibrator}. */
+@Implements(value = SystemVibrator.class, isInAndroidSdk = false, looseSignatures = true)
 public class ShadowSystemVibrator extends ShadowVibrator {
 
   private final Handler handler = new Handler(Looper.getMainLooper());
@@ -133,11 +134,14 @@
 
   @Implementation(minSdk = S)
   protected void vibrate(
-      int uid,
-      String opPkg,
-      VibrationEffect effect,
-      String reason,
-      VibrationAttributes attributes) {
+      Object uid, Object opPkg, Object effect, Object reason, Object attributes) {
+    Preconditions.checkArgument(uid instanceof Integer);
+    Preconditions.checkArgument(opPkg == null || opPkg instanceof String);
+    // The SystemVibrator#vibrate needs effect NonNull.
+    Preconditions.checkArgument(effect instanceof VibrationEffect);
+    Preconditions.checkArgument(reason == null || reason instanceof String);
+    // The SystemVibrator#vibrate needs attributes NonNull.
+    Preconditions.checkArgument(attributes instanceof android.os.VibrationAttributes);
     if (effect instanceof VibrationEffect.Composed) {
       VibrationEffect.Composed composedEffect = (VibrationEffect.Composed) effect;
       vibrationAttributesFromLastVibration = attributes;
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 0864354..048abf0 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowTelephonyManager.java
@@ -52,6 +52,7 @@
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyManager.CellInfoCallback;
 import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.telephony.emergency.EmergencyNumber;
 import android.text.TextUtils;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -59,13 +60,16 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.HiddenApi;
@@ -141,6 +145,7 @@
   private String visualVoicemailPackageName = null;
   private SignalStrength signalStrength;
   private boolean dataEnabled = false;
+  private final Set<Integer> dataDisabledReasons = new HashSet<>();
   private boolean isRttSupported;
   private final List<String> sentDialerSpecialCodes = new ArrayList<>();
   private boolean hearingAidCompatibilitySupported = false;
@@ -152,6 +157,7 @@
   private VisualVoicemailSmsParams lastVisualVoicemailSmsParams;
   private VisualVoicemailSmsFilterSettings visualVoicemailSmsFilterSettings;
   private boolean emergencyCallbackMode;
+  private static Map<Integer, List<EmergencyNumber>> emergencyNumbersList;
 
   /**
    * Should be {@link TelephonyManager.BootstrapAuthenticationCallback} but this object was
@@ -169,6 +175,7 @@
   @Resetter
   public static void reset() {
     callComposerStatus = 0;
+    emergencyNumbersList = null;
   }
 
   @Implementation(minSdk = S)
@@ -263,6 +270,13 @@
   }
 
   /** Call state may be specified via {@link #setCallState(int)}. */
+  @Implementation(minSdk = S)
+  protected int getCallStateForSubscription() {
+    checkReadPhoneStatePermission();
+    return callState;
+  }
+
+  /** Call state may be specified via {@link #setCallState(int)}. */
   @Implementation
   protected int getCallState() {
     checkReadPhoneStatePermission();
@@ -1215,12 +1229,39 @@
   }
 
   /**
+   * Implementation for {@link TelephonyManager#isDataEnabledForReason}.
+   *
+   * @return True by default, unless reason is set to false with {@link
+   *     TelephonyManager#setDataEnabledForReason}.
+   */
+  @Implementation(minSdk = Build.VERSION_CODES.S)
+  protected boolean isDataEnabledForReason(@TelephonyManager.DataEnabledReason int reason) {
+    checkReadPhoneStatePermission();
+    return !dataDisabledReasons.contains(reason);
+  }
+
+  /**
    * Implementation for {@link TelephonyManager#setDataEnabled}. Marked as public in order to allow
    * it to be used as a test API.
    */
   @Implementation(minSdk = Build.VERSION_CODES.O)
   public void setDataEnabled(boolean enabled) {
-    dataEnabled = enabled;
+    setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER, enabled);
+  }
+
+  /**
+   * Implementation for {@link TelephonyManager#setDataEnabledForReason}. Marked as public in order
+   * to allow it to be used as a test API.
+   */
+  @Implementation(minSdk = Build.VERSION_CODES.S)
+  public void setDataEnabledForReason(
+      @TelephonyManager.DataEnabledReason int reason, boolean enabled) {
+    if (enabled) {
+      dataDisabledReasons.remove(reason);
+    } else {
+      dataDisabledReasons.add(reason);
+    }
+    dataEnabled = dataDisabledReasons.isEmpty();
   }
 
   /**
@@ -1374,4 +1415,25 @@
       return sentIntent;
     }
   }
+
+  /**
+   * Sets the emergency numbers list returned by {@link TelephonyManager#getEmergencyNumberList}.
+   */
+  public static void setEmergencyNumberList(
+      Map<Integer, List<EmergencyNumber>> emergencyNumbersList) {
+    ShadowTelephonyManager.emergencyNumbersList = emergencyNumbersList;
+  }
+
+  /**
+   * Implementation for {@link TelephonyManager#getEmergencyNumberList}.
+   *
+   * @return an immutable map by default, unless set with {@link #setEmergencyNumberList}.
+   */
+  @Implementation(minSdk = R)
+  protected Map<Integer, List<EmergencyNumber>> getEmergencyNumberList() {
+    if (ShadowTelephonyManager.emergencyNumbersList != null) {
+      return ShadowTelephonyManager.emergencyNumbersList;
+    }
+    return ImmutableMap.of();
+  }
 }
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 5c8de73..00ad658 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowUserManager.java
@@ -600,6 +600,16 @@
   }
 
   @HiddenApi
+  @Implementation(minSdk = R)
+  protected List<UserHandle> getUserHandles(boolean excludeDying) {
+    ArrayList<UserHandle> userHandles = new ArrayList<>();
+    for (int id : userManagerState.userSerialNumbers.keySet()) {
+      userHandles.addAll(userManagerState.userProfilesListMap.get(id));
+    }
+    return userHandles;
+  }
+
+  @HiddenApi
   @Implementation(minSdk = JELLY_BEAN_MR1)
   protected static int getMaxSupportedUsers() {
     return maxSupportedUsers;
@@ -998,6 +1008,9 @@
 
   @Implementation(minSdk = JELLY_BEAN_MR1)
   protected boolean removeUser(int userHandle) {
+    if (!userManagerState.userInfoMap.containsKey(userHandle)) {
+      return false;
+    }
     userManagerState.userInfoMap.remove(userHandle);
     userManagerState.userPidMap.remove(userHandle);
     userManagerState.userSerialNumbers.remove(userHandle);
@@ -1021,6 +1034,13 @@
     return removeUser(user.getIdentifier());
   }
 
+  @Implementation(minSdk = TIRAMISU)
+  protected int removeUserWhenPossible(UserHandle user, boolean overrideDevicePolicy) {
+    return removeUser(user.getIdentifier())
+        ? UserManager.REMOVE_RESULT_REMOVED
+        : UserManager.REMOVE_RESULT_ERROR_UNKNOWN;
+  }
+
   @Implementation(minSdk = N)
   protected static boolean supportsMultipleUsers() {
     return isMultiUserSupported;
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 b66a0a4..276a31d 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVibrator.java
@@ -3,18 +3,19 @@
 import static android.os.Build.VERSION_CODES.R;
 
 import android.media.AudioAttributes;
-import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
-import android.os.vibrator.VibrationEffectSegment;
+import android.os.vibrator.PrimitiveSegment;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.Resetter;
+import org.robolectric.util.ReflectionHelpers;
 
 @Implements(Vibrator.class)
 public class ShadowVibrator {
@@ -22,10 +23,10 @@
   static boolean cancelled;
   static long milliseconds;
   protected static long[] pattern;
-  protected static final List<VibrationEffectSegment> vibrationEffectSegments = new ArrayList<>();
+  protected static final List<Object> 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 Object vibrationAttributesFromLastVibration;
   @Nullable protected static AudioAttributes audioAttributesFromLastVibration;
   static int repeat;
   static boolean hasVibrator = true;
@@ -81,9 +82,18 @@
     return repeat;
   }
 
-  /** Returns the last list of {@link VibrationEffectSegment}. */
-  public List<VibrationEffectSegment> getVibrationEffectSegments() {
-    return vibrationEffectSegments;
+  /** Returns the last list of {@link PrimitiveSegment} vibrations in {@link PrimitiveEffect}. */
+  @SuppressWarnings("JdkCollectors") // toImmutableList is only supported in Java 8+.
+  public List<PrimitiveEffect> getPrimitiveSegmentsInPrimitiveEffects() {
+    return vibrationEffectSegments.stream()
+        .filter(segment -> segment instanceof PrimitiveSegment)
+        .map(
+            segment ->
+                new PrimitiveEffect(
+                    ReflectionHelpers.getField(segment, "mPrimitiveId"),
+                    ReflectionHelpers.getField(segment, "mScale"),
+                    ReflectionHelpers.getField(segment, "mDelay")))
+        .collect(Collectors.toList());
   }
 
   /** Returns the last list of {@link PrimitiveEffect}. */
@@ -108,9 +118,9 @@
     supportedPrimitives.addAll(primitives);
   }
 
-  /** Returns the {@link VibrationAttributes} from the last vibration. */
+  /** Returns the {@link android.os.VibrationAttributes} from the last vibration. */
   @Nullable
-  public VibrationAttributes getVibrationAttributesFromLastVibration() {
+  public Object getVibrationAttributesFromLastVibration() {
     return vibrationAttributesFromLastVibration;
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
index 70cb369..5d06f58 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowView.java
@@ -524,31 +524,29 @@
 
   @Implementation
   protected boolean post(Runnable action) {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      return reflector(_View_.class, realView).post(action);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowApplication.getInstance().getForegroundThreadScheduler().post(action);
       return true;
+    } else {
+      return reflector(_View_.class, realView).post(action);
     }
   }
 
   @Implementation
   protected boolean postDelayed(Runnable action, long delayMills) {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      return reflector(_View_.class, realView).postDelayed(action, delayMills);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowApplication.getInstance()
           .getForegroundThreadScheduler()
           .postDelayed(action, delayMills);
       return true;
+    } else {
+      return reflector(_View_.class, realView).postDelayed(action, delayMills);
     }
   }
 
   @Implementation
   protected void postInvalidateDelayed(long delayMilliseconds) {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowApplication.getInstance()
           .getForegroundThreadScheduler()
           .postDelayed(
@@ -559,17 +557,19 @@
                 }
               },
               delayMilliseconds);
+    } else {
+      reflector(_View_.class, realView).postInvalidateDelayed(delayMilliseconds);
     }
   }
 
   @Implementation
   protected boolean removeCallbacks(Runnable callback) {
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      return reflector(_View_.class, realView).removeCallbacks(callback);
-    } else {
+    if (ShadowLooper.looperMode() == LooperMode.Mode.LEGACY) {
       ShadowLegacyLooper shadowLooper = Shadow.extract(Looper.getMainLooper());
       shadowLooper.getScheduler().remove(callback);
       return true;
+    } else {
+      return reflector(_View_.class, realView).removeCallbacks(callback);
     }
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
index 28e6680..2f13fcf 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowViewGroup.java
@@ -9,7 +9,7 @@
 import java.io.PrintStream;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 import org.robolectric.annotation.RealObject;
 import org.robolectric.shadow.api.Shadow;
 import org.robolectric.util.reflector.Direct;
@@ -29,10 +29,10 @@
         () -> {
           reflector(ViewGroupReflector.class, realViewGroup).addView(child, index, params);
         };
-    if (ShadowLooper.looperMode() == LooperMode.Mode.PAUSED) {
-      addViewRunnable.run();
-    } else {
+    if (ShadowLooper.looperMode() == Mode.LEGACY) {
       shadowMainLooper().runPaused(addViewRunnable);
+    } else {
+      addViewRunnable.run();
     }
   }
 
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
new file mode 100644
index 0000000..99f807b
--- /dev/null
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowVpnManager.java
@@ -0,0 +1,67 @@
+package org.robolectric.shadows;
+
+import android.content.Intent;
+import android.net.PlatformVpnProfile;
+import android.net.VpnManager;
+import android.net.VpnProfileState;
+import android.os.Build.VERSION_CODES;
+import java.util.UUID;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for {@link VpnManager}. */
+@Implements(value = VpnManager.class, minSdk = VERSION_CODES.R)
+public class ShadowVpnManager {
+
+  private VpnProfileState vpnProfileState;
+  private Intent provisionVpnProfileIntent;
+
+  @Implementation
+  protected void deleteProvisionedVpnProfile() {
+    vpnProfileState = null;
+  }
+
+  @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+  protected VpnProfileState getProvisionedVpnProfileState() {
+    return vpnProfileState;
+  }
+
+  /**
+   * @see #setProvisionVpnProfileResult(Intent).
+   */
+  @Implementation
+  protected Intent provisionVpnProfile(PlatformVpnProfile profile) {
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+      vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+    }
+    return provisionVpnProfileIntent;
+  }
+
+  /** Sets the return value of #provisionVpnProfile(PlatformVpnProfile). */
+  public void setProvisionVpnProfileResult(Intent intent) {
+    provisionVpnProfileIntent = intent;
+  }
+
+  @Implementation
+  protected void startProvisionedVpnProfile() {
+    startProvisionedVpnProfileSession();
+  }
+
+  @Implementation(minSdk = VERSION_CODES.TIRAMISU)
+  protected String startProvisionedVpnProfileSession() {
+    String sessionKey = UUID.randomUUID().toString();
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+      vpnProfileState =
+          new VpnProfileState(VpnProfileState.STATE_CONNECTED, sessionKey, false, false);
+    }
+    return sessionKey;
+  }
+
+  @Implementation
+  protected void stopProvisionedVpnProfile() {
+    if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.TIRAMISU) {
+      vpnProfileState = new VpnProfileState(VpnProfileState.STATE_DISCONNECTED, null, false, false);
+    }
+  }
+}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
index 706d86e..1a8131f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWebView.java
@@ -569,7 +569,7 @@
    *
    * @param canGoBack Value to return from {@code android.webkit.WebView#canGoBack()}
    * @deprecated Do not depend on this method as it will be removed in a future update. The
-   *     preferered method is to populate a fake web history to use for going back.
+   *     preferred method is to populate a fake web history to use for going back.
    */
   @Deprecated
   public void setCanGoBack(boolean canGoBack) {
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 8e933d9..7221e69 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowWifiManager.java
@@ -5,6 +5,9 @@
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 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 java.util.stream.Collectors.toList;
 
 import android.content.Context;
 import android.content.Intent;
@@ -17,14 +20,19 @@
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.MulticastLock;
+import android.net.wifi.WifiSsid;
 import android.net.wifi.WifiUsabilityStatsEntry;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.Pair;
 import com.google.common.collect.ImmutableList;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -71,6 +79,8 @@
   @RealObject WifiManager wifiManager;
   private WifiConfiguration apConfig;
   private SoftApConfiguration softApConfig;
+  private final Object pnoRequestLock = new Object();
+  private PnoScanRequest outstandingPnoScanRequest = null;
 
   @Implementation
   protected boolean setWifiEnabled(boolean wifiEnabled) {
@@ -657,4 +667,176 @@
       this.predictionHorizonSec = predictionHorizonSec;
     }
   }
+
+  /** Informs the {@link WifiManager} of a list of PNO {@link ScanResult}. */
+  public void networksFoundFromPnoScan(List<ScanResult> scanResults) {
+    synchronized (pnoRequestLock) {
+      List<ScanResult> scanResultsCopy = List.copyOf(scanResults);
+      if (outstandingPnoScanRequest == null
+          || outstandingPnoScanRequest.ssids.stream()
+              .noneMatch(
+                  ssid ->
+                      scanResultsCopy.stream()
+                          .anyMatch(scanResult -> scanResult.getWifiSsid().equals(ssid)))) {
+        return;
+      }
+      Executor executor = outstandingPnoScanRequest.executor;
+      InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+      executor.execute(() -> callback.onScanResultsAvailable(scanResultsCopy));
+      Intent intent = createPnoScanResultsBroadcastIntent();
+      getContext().sendBroadcast(intent);
+      executor.execute(
+          () ->
+              callback.onRemoved(
+                  InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_RESULTS_DELIVERED));
+      outstandingPnoScanRequest = null;
+    }
+  }
+
+  // Object needs to be used here since PnoScanResultsCallback is hidden. The looseSignatures spec
+  // requires that all args are of type Object.
+  @Implementation(minSdk = TIRAMISU)
+  @HiddenApi
+  protected void setExternalPnoScanRequest(
+      Object ssids, Object frequencies, Object executor, Object callback) {
+    synchronized (pnoRequestLock) {
+      if (callback == null) {
+        throw new IllegalArgumentException("callback cannot be null");
+      }
+
+      List<WifiSsid> pnoSsids = (List<WifiSsid>) ssids;
+      int[] pnoFrequencies = (int[]) frequencies;
+      Executor pnoExecutor = (Executor) executor;
+      InternalPnoScanResultsCallback pnoCallback = new InternalPnoScanResultsCallback(callback);
+
+      if (pnoExecutor == null) {
+        throw new IllegalArgumentException("executor cannot be null");
+      }
+      if (pnoSsids == null || pnoSsids.isEmpty()) {
+        // The real WifiServiceImpl throws an IllegalStateException in this case, so keeping it the
+        // same for consistency.
+        throw new IllegalStateException("Ssids can't be null or empty");
+      }
+      if (pnoSsids.size() > 2) {
+        throw new IllegalArgumentException("Ssid list can't be greater than 2");
+      }
+      if (pnoFrequencies != null && pnoFrequencies.length > 10) {
+        throw new IllegalArgumentException("Length of frequencies must be smaller than 10");
+      }
+      int uid = Binder.getCallingUid();
+      String packageName = getContext().getPackageName();
+
+      if (outstandingPnoScanRequest != null) {
+        pnoExecutor.execute(
+            () ->
+                pnoCallback.onRegisterFailed(
+                    uid == outstandingPnoScanRequest.uid
+                        ? InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_ALREADY_REGISTERED
+                        : InternalPnoScanResultsCallback.REGISTER_PNO_CALLBACK_RESOURCE_BUSY));
+        return;
+      }
+
+      outstandingPnoScanRequest =
+          new PnoScanRequest(pnoSsids, pnoFrequencies, pnoExecutor, pnoCallback, packageName, uid);
+      pnoExecutor.execute(pnoCallback::onRegisterSuccess);
+    }
+  }
+
+  @Implementation(minSdk = TIRAMISU)
+  @HiddenApi
+  protected void clearExternalPnoScanRequest() {
+    synchronized (pnoRequestLock) {
+      if (outstandingPnoScanRequest != null
+          && outstandingPnoScanRequest.uid == Binder.getCallingUid()) {
+        InternalPnoScanResultsCallback callback = outstandingPnoScanRequest.callback;
+        outstandingPnoScanRequest.executor.execute(
+            () ->
+                callback.onRemoved(
+                    InternalPnoScanResultsCallback.REMOVE_PNO_CALLBACK_UNREGISTERED));
+        outstandingPnoScanRequest = null;
+      }
+    }
+  }
+
+  private static class PnoScanRequest {
+    private final List<WifiSsid> ssids;
+    private final List<Integer> frequencies;
+    private final Executor executor;
+    private final InternalPnoScanResultsCallback callback;
+    private final String packageName;
+    private final int uid;
+
+    private PnoScanRequest(
+        List<WifiSsid> ssids,
+        int[] frequencies,
+        Executor executor,
+        InternalPnoScanResultsCallback callback,
+        String packageName,
+        int uid) {
+      this.ssids = List.copyOf(ssids);
+      this.frequencies =
+          frequencies == null ? List.of() : Arrays.stream(frequencies).boxed().collect(toList());
+      this.executor = executor;
+      this.callback = callback;
+      this.packageName = packageName;
+      this.uid = uid;
+    }
+  }
+
+  private Intent createPnoScanResultsBroadcastIntent() {
+    Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+    intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
+    intent.setPackage(outstandingPnoScanRequest.packageName);
+    return intent;
+  }
+
+  private static class InternalPnoScanResultsCallback {
+    static final int REGISTER_PNO_CALLBACK_ALREADY_REGISTERED = 1;
+    static final int REGISTER_PNO_CALLBACK_RESOURCE_BUSY = 2;
+    static final int REMOVE_PNO_CALLBACK_RESULTS_DELIVERED = 1;
+    static final int REMOVE_PNO_CALLBACK_UNREGISTERED = 2;
+
+    final Object callback;
+    final Method availableCallback;
+    final Method successCallback;
+    final Method failedCallback;
+    final Method removedCallback;
+
+    InternalPnoScanResultsCallback(Object callback) {
+      this.callback = callback;
+      try {
+        Class<?> pnoCallbackClass = callback.getClass();
+        availableCallback = pnoCallbackClass.getMethod("onScanResultsAvailable", List.class);
+        successCallback = pnoCallbackClass.getMethod("onRegisterSuccess");
+        failedCallback = pnoCallbackClass.getMethod("onRegisterFailed", int.class);
+        removedCallback = pnoCallbackClass.getMethod("onRemoved", int.class);
+      } catch (NoSuchMethodException e) {
+        throw new IllegalArgumentException("callback is not of type PnoScanResultsCallback", e);
+      }
+    }
+
+    void onScanResultsAvailable(List<ScanResult> scanResults) {
+      invokeCallback(availableCallback, scanResults);
+    }
+
+    void onRegisterSuccess() {
+      invokeCallback(successCallback);
+    }
+
+    void onRegisterFailed(int reason) {
+      invokeCallback(failedCallback, reason);
+    }
+
+    void onRemoved(int reason) {
+      invokeCallback(removedCallback, reason);
+    }
+
+    void invokeCallback(Method method, Object... args) {
+      try {
+        method.invoke(callback, args);
+      } catch (IllegalAccessException | InvocationTargetException e) {
+        throw new IllegalStateException("Failed to invoke " + method.getName(), e);
+      }
+    }
+  }
 }
diff --git a/shadows/httpclient/build.gradle b/shadows/httpclient/build.gradle
index 332c83e..4e77a09 100644
--- a/shadows/httpclient/build.gradle
+++ b/shadows/httpclient/build.gradle
@@ -21,17 +21,17 @@
     api project(":utils")
 
     // We should keep httpclient version for low level API compatibility.
-    earlyRuntime "org.apache.httpcomponents:httpcore:4.0.1"
-    api "org.apache.httpcomponents:httpclient:4.0.3"
-    compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true }
+    earlyRuntime libs.apache.http.core
+    api libs.apache.http.client
+    compileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates)
 
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
     testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
 
-    testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates) { force = true }
+    testCompileOnly(AndroidSdk.LOLLIPOP_MR1.coordinates)
     testRuntimeOnly AndroidSdk.S.coordinates
 }
 
diff --git a/shadows/playservices/build.gradle b/shadows/playservices/build.gradle
index c3abbba..b009838 100644
--- a/shadows/playservices/build.gradle
+++ b/shadows/playservices/build.gradle
@@ -14,25 +14,20 @@
 dependencies {
     compileOnly project(":shadows:framework")
     api project(":annotations")
-    api "com.google.guava:guava:$guavaJREVersion"
+    api libs.guava
 
-    compileOnly "androidx.fragment:fragment:1.2.0"
-    compileOnly "com.google.android.gms:play-services-base:8.4.0"
-    compileOnly "com.google.android.gms:play-services-basement:8.4.0"
+    compileOnly libs.bundles.play.services.base.for.shadows
 
     compileOnly AndroidSdk.MAX_SDK.coordinates
 
     testCompileOnly AndroidSdk.MAX_SDK.coordinates
-    testCompileOnly "com.google.android.gms:play-services-base:8.4.0"
-    testCompileOnly "com.google.android.gms:play-services-basement:8.4.0"
+    testCompileOnly libs.bundles.play.services.base.for.shadows
 
     testImplementation project(":robolectric")
-    testImplementation "junit:junit:$junitVersion"
-    testImplementation "com.google.truth:truth:$truthVersion"
-    testImplementation "org.mockito:mockito-core:$mockitoVersion"
-    testRuntimeOnly "androidx.fragment:fragment:1.2.0"
-    testRuntimeOnly "com.google.android.gms:play-services-base:8.4.0"
-    testRuntimeOnly "com.google.android.gms:play-services-basement:8.4.0"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.mockito
+    testRuntimeOnly libs.bundles.play.services.base.for.shadows
 
     testRuntimeOnly AndroidSdk.MAX_SDK.coordinates
 }
diff --git a/shadows/versioning/Android.bp b/shadows/versioning/Android.bp
new file mode 100644
index 0000000..b630e22
--- /dev/null
+++ b/shadows/versioning/Android.bp
@@ -0,0 +1,67 @@
+//#############################################
+// Compile Robolectric utils
+//#############################################
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "external_robolectric_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["external_robolectric_license"],
+}
+
+java_library_host {
+    name: "Robolectric_shadows_versioning_upstream",
+    srcs: ["src/main/java/**/*.java"],
+    static_libs: [
+        "robolectric-javax.annotation-api-1.2",
+        "Robolectric_shadowapi_upstream",
+        "Robolectric_utils_upstream",
+        "jsr305",
+    ],
+    libs: ["robolectric-host-android_all_upstream"],
+}
+
+//#############################################
+// Compile Robolectric utils tests
+//#############################################
+
+java_test_host {
+    name: "Robolectric_shadows_versioning_tests_upstream",
+    srcs: ["src/test/java/**/AndroidVersionsEdgeCaseTest.java"],
+    static_libs: [
+        "Robolectric_shadows_versioning_upstream",
+        "hamcrest",
+        "guava",
+        "junit",
+        "truth-prebuilt",
+    ],
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+}
+
+//android_robolectric_test {
+//    enabled: true,
+//
+//    name: "Robolectric_shadows_versioning_tests_e2e_upstream",
+//
+//    srcs: [
+//        "src/**/*.AndroidVersionsTest.java",
+//    ],
+//
+//    java_resource_dirs: ["config"],
+//
+//    libs: [
+//        "androidx.test.core",
+//        "androidx.test.runner",
+//    ],
+//
+//    instrumentation_for: "MyRoboApplication",
+//
+//    upstream: true,
+//}
+
+
diff --git a/shadows/versioning/build.gradle b/shadows/versioning/build.gradle
new file mode 100644
index 0000000..68a8fb7
--- /dev/null
+++ b/shadows/versioning/build.gradle
@@ -0,0 +1,21 @@
+import org.robolectric.gradle.DeployedRoboJavaModulePlugin
+import org.robolectric.gradle.RoboJavaModulePlugin
+
+apply plugin: RoboJavaModulePlugin
+apply plugin: DeployedRoboJavaModulePlugin
+
+configurations {
+    earlyRuntime
+}
+
+dependencies {
+    api project(":shadowapi")
+    compileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK (AndroidSdk.s.coordinates) { force = true }
+    testImplementation project(":robolectric")
+    testImplementation libs.truth
+    testImplementation "androidx.test.ext:junit:$axtJunitVersion@aar"
+    testCompileOnly AndroidSdk.MAX_SDK.coordinates // compile against latest Android SDK
+    testRuntimeOnly AndroidSdk.MAX_SDK.coordinates // run against whatever this JDK supports
+}
+
+
diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java
new file mode 100644
index 0000000..316365b
--- /dev/null
+++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersionInitTools.java
@@ -0,0 +1,23 @@
+package org.robolectric.versioning;
+
+import java.io.IOException;
+import java.util.Properties;
+import java.util.jar.JarFile;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
+
+/**
+ * Utility access method to allow robolectric to instantiate AndroidVersions without cluttering code
+ * completion for users of AndroidVersions's embedded Types of one per Android Releases.
+ */
+public final class AndroidVersionInitTools {
+
+  private AndroidVersionInitTools() {}
+
+  public static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException {
+    return AndroidVersions.computeReleaseVersion(jarFile);
+  }
+
+  public static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) {
+    return AndroidVersions.computeCurrentSdkFromBuildProps(buildProps);
+  }
+}
diff --git a/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
new file mode 100644
index 0000000..6314518
--- /dev/null
+++ b/shadows/versioning/src/main/java/org/robolectric/versioning/AndroidVersions.java
@@ -0,0 +1,779 @@
+package org.robolectric.versioning;
+
+/*
+ * Copyright (C) 2023 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 static java.util.Arrays.asList;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import javax.annotation.Nullable;
+import org.robolectric.util.Logger;
+import org.robolectric.util.ReflectionHelpers;
+
+/**
+ * Android versioning is complicated.<br>
+ * 1) There is a yearly letter release with an increasing of one alpha step each year A-> B, B-> C,
+ * and so on. While commonly referenced these are not the release numbers. This class calls these
+ * shortcodes. Also minor version number releases (usually within the same year) will start with the
+ * same letter.<br>
+ * 2) There is an SDK_INT field in android.os.Build.VERSION that tracks a version of the internal
+ * SDK. While useful to track the actual released versions of Android, these are not the release
+ * number. More importantly, android.os.Build.VERSION uses code names to describe future versions.
+ * Multiple code names may be in development at once on different branches of Android.<br>
+ * 3) There is a yearly release major number followed by a minor number, which may or may not be
+ * used.<br>
+ * 4) Relevant logic and reasoning should match androidx.core.os.BuildCompat.java with the caveat
+ * that this class guess at the future release version number and short of the current dev branch.
+ * <br>
+ */
+public final class AndroidVersions {
+
+  private AndroidVersions() {}
+
+  /** Representation of an android release, one that has occurred, or is expected. */
+  public abstract static class AndroidRelease implements Comparable<AndroidRelease> {
+
+    /**
+     * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt may
+     * still be that of the prior release.
+     */
+    public int getSdkInt() {
+      return ReflectionHelpers.getStaticField(this.getClass(), "SDK_INT");
+    }
+
+    /**
+     * single character short code for the release, multiple characters for minor releases (only
+     * minor version numbers increment - usually within the same year).
+     */
+    public String getShortCode() {
+      return ReflectionHelpers.getStaticField(this.getClass(), "SHORT_CODE");
+    }
+
+    /**
+     * true if this release has already occurred, false otherwise. If unreleased, the getSdkInt will
+     * guess at the likely sdk number. Your code will need to recompile if this value changes -
+     * including most modern build tools; bazle, soong all are full build systems - and as such
+     * organizations using them have no concerns.
+     */
+    public boolean isReleased() {
+      return ReflectionHelpers.getStaticField(this.getClass(), "RELEASED");
+    }
+
+    /** major.minor version number as String. */
+    public String getVersion() {
+      return ReflectionHelpers.getStaticField(this.getClass(), "VERSION");
+    }
+
+    /**
+     * Implements comparable.
+     *
+     * @param other the object to be compared.
+     * @return 1 if this is greater than other, 0 if equal, -1 if less
+     * @throws RuntimeException if other is not an instance of AndroidRelease.
+     */
+    @Override
+    public int compareTo(AndroidRelease other) {
+      if (other == null) {
+        throw new RuntimeException(
+            "Only "
+                + AndroidVersions.class.getName()
+                + " should define Releases, illegal class "
+                + other.getClass());
+      }
+      return Integer.compare(this.getSdkInt(), other.getSdkInt());
+    }
+
+    @Override
+    public String toString() {
+      return "Android "
+          + (this.isReleased() ? "" : "Future ")
+          + "Release: "
+          + this.getVersion()
+          + " ( sdk: "
+          + this.getSdkInt()
+          + " code: "
+          + this.getShortCode()
+          + " )";
+    }
+  }
+
+  /**
+   * Version: 4.1 <br>
+   * ShortCode: J <br>
+   * SDK API Level: 16 <br>
+   * release: true <br>
+   */
+  public static final class J extends AndroidRelease {
+
+    public static final int SDK_INT = 16;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "J";
+
+    public static final String VERSION = "4.1";
+  }
+
+  /**
+   * Version: 4.2 <br>
+   * ShortCode: JMR1 <br>
+   * SDK API Level: 17 <br>
+   * release: true <br>
+   */
+  public static final class JMR1 extends AndroidRelease {
+
+    public static final int SDK_INT = 17;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "JMR1";
+
+    public static final String VERSION = "4.2";
+  }
+
+  /**
+   * Version: 4.3 <br>
+   * ShortCode: JMR2 <br>
+   * SDK API Level: 18 <br>
+   * release: true <br>
+   */
+  public static final class JMR2 extends AndroidRelease {
+
+    public static final int SDK_INT = 18;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "JMR2";
+
+    public static final String VERSION = "4.3";
+  }
+
+  /**
+   * Version: 4.4 <br>
+   * ShortCode: K <br>
+   * SDK API Level: 19 <br>
+   * release: true <br>
+   */
+  public static final class K extends AndroidRelease {
+
+    public static final int SDK_INT = 19;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "K";
+
+    public static final String VERSION = "4.4";
+  }
+
+  // Skipping K Watch release, which was 20.
+
+  /**
+   * Version: 5.0 <br>
+   * ShortCode: L <br>
+   * SDK API Level: 21 <br>
+   * release: true <br>
+   */
+  public static final class L extends AndroidRelease {
+
+    public static final int SDK_INT = 21;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "L";
+
+    public static final String VERSION = "5.0";
+  }
+
+  /**
+   * Version: 5.1 <br>
+   * ShortCode: LMR1 <br>
+   * SDK API Level: 22 <br>
+   * release: true <br>
+   */
+  public static final class LMR1 extends AndroidRelease {
+
+    public static final int SDK_INT = 22;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "LMR1";
+
+    public static final String VERSION = "5.1";
+  }
+
+  /**
+   * Version: 6.0 <br>
+   * ShortCode: M <br>
+   * SDK API Level: 23 <br>
+   * release: true <br>
+   */
+  public static final class M extends AndroidRelease {
+
+    public static final int SDK_INT = 23;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "M";
+
+    public static final String VERSION = "6.0";
+  }
+
+  /**
+   * Version: 7.0 <br>
+   * ShortCode: N <br>
+   * SDK API Level: 24 <br>
+   * release: true <br>
+   */
+  public static final class N extends AndroidRelease {
+
+    public static final int SDK_INT = 24;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "N";
+
+    public static final String VERSION = "7.0";
+  }
+
+  /**
+   * Release: 7.1 <br>
+   * ShortCode: NMR1 <br>
+   * SDK Framework: 25 <br>
+   * release: true <br>
+   */
+  public static final class NMR1 extends AndroidRelease {
+
+    public static final int SDK_INT = 25;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "NMR1";
+
+    private static final String VERSION = "7.1";
+  }
+
+  /**
+   * Release: 8.0 <br>
+   * ShortCode: O <br>
+   * SDK API Level: 26 <br>
+   * release: true <br>
+   */
+  public static final class O extends AndroidRelease {
+
+    public static final int SDK_INT = 26;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "O";
+
+    public static final String VERSION = "8.0";
+  }
+
+  /**
+   * Release: 8.1 <br>
+   * ShortCode: OMR1 <br>
+   * SDK API Level: 27 <br>
+   * release: true <br>
+   */
+  public static final class OMR1 extends AndroidRelease {
+
+    public static final int SDK_INT = 27;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "OMR1";
+
+    public static final String VERSION = "8.1";
+  }
+
+  /**
+   * Release: 9.0 <br>
+   * ShortCode: P <br>
+   * SDK API Level: 28 <br>
+   * release: true <br>
+   */
+  public static final class P extends AndroidRelease {
+
+    public static final int SDK_INT = 28;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "P";
+
+    public static final String VERSION = "9.0";
+  }
+
+  /**
+   * Release: 10.0 <br>
+   * ShortCode: Q <br>
+   * SDK API Level: 29 <br>
+   * release: true <br>
+   */
+  public static final class Q extends AndroidRelease {
+
+    public static final int SDK_INT = 29;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "Q";
+
+    public static final String VERSION = "10.0";
+  }
+
+  /**
+   * Release: 11.0 <br>
+   * ShortCode: R <br>
+   * SDK API Level: 30 <br>
+   * release: true <br>
+   */
+  public static final class R extends AndroidRelease {
+
+    public static final int SDK_INT = 30;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "R";
+
+    public static final String VERSION = "11.0";
+  }
+
+  /**
+   * Release: 12.0 <br>
+   * ShortCode: S <br>
+   * SDK API Level: 31 <br>
+   * release: true <br>
+   */
+  public static final class S extends AndroidRelease {
+
+    public static final int SDK_INT = 31;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "S";
+
+    public static final String VERSION = "12.0";
+  }
+
+  /**
+   * Release: 12.1 <br>
+   * ShortCode: Sv2 <br>
+   * SDK API Level: 32 <br>
+   * release: true <br>
+   */
+  @SuppressWarnings("UPPER_SNAKE_CASE")
+  public static final class Sv2 extends AndroidRelease {
+
+    public static final int SDK_INT = 32;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "Sv2";
+
+    public static final String VERSION = "12.1";
+  }
+
+  /**
+   * Release: 13.0 <br>
+   * ShortCode: T <br>
+   * SDK API Level: 33 <br>
+   * release: true <br>
+   */
+  public static final class T extends AndroidRelease {
+
+    public static final int SDK_INT = 33;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "T";
+
+    public static final String VERSION = "13.0";
+  }
+
+  /**
+   * Potential Release: 14.0 <br>
+   * ShortCode: U <br>
+   * SDK API Level: 34 <br>
+   * release: false <br>
+   */
+  public static final class U extends AndroidRelease {
+
+    public static final int SDK_INT = 34;
+
+    public static final boolean RELEASED = true;
+
+    public static final String SHORT_CODE = "U";
+
+    public static final String VERSION = "14.0";
+  }
+
+  /**
+   * Potential Release: 15.0 <br>
+   * ShortCode: V <br>
+   * SDK API Level: 34+ <br>
+   * release: false <br>
+   */
+  public static final class V extends AndroidRelease {
+
+    public static final int SDK_INT = 35;
+
+    public static final boolean RELEASED = false;
+
+    public static final String SHORT_CODE = "V";
+
+    public static final String VERSION = "15.0";
+  }
+
+  /** The current release this process is running on. */
+  public static final AndroidRelease CURRENT;
+
+  @Nullable
+  public static AndroidRelease getReleaseForSdkInt(@Nullable Integer sdkInt) {
+    if (sdkInt == null) {
+      return null;
+    } else {
+      return information.sdkIntToAllReleases.get(sdkInt);
+    }
+  }
+
+  public static List<AndroidRelease> getReleases() {
+    List<AndroidRelease> output = new ArrayList<>();
+    for (AndroidRelease release : information.allReleases) {
+      if (release.isReleased()) {
+        output.add(release);
+      }
+    }
+    return output;
+  }
+
+  public static List<AndroidRelease> getUnreleased() {
+    List<AndroidRelease> output = new ArrayList<>();
+    for (AndroidRelease release : information.allReleases) {
+      if (!release.isReleased()) {
+        output.add(release);
+      }
+    }
+    return output;
+  }
+
+  /**
+   * Responsible for aggregating and interpreting the static state representing the current
+   * AndroidReleases known to AndroidVersions class.
+   */
+  static class SdkInformation {
+    final List<AndroidRelease> allReleases;
+    final List<Class<? extends AndroidRelease>> classesWithIllegalNames;
+    final AndroidRelease latestRelease;
+    final AndroidRelease earliestUnreleased;
+
+    // In the future we may need a multimap for sdkInts should they stay static across releases.
+    final Map<Integer, AndroidRelease> sdkIntToAllReleases = new HashMap<>();
+    final Map<String, AndroidRelease> shortCodeToAllReleases = new HashMap<>();
+
+    // detected errors
+    final List<Map.Entry<AndroidRelease, AndroidRelease>> sdkIntCollisions = new ArrayList<>();
+    Map.Entry<AndroidRelease, AndroidRelease> sdkApiMisordered = null;
+
+    public SdkInformation(
+        List<AndroidRelease> releases,
+        List<Class<? extends AndroidRelease>> classesWithIllegalNames) {
+      this.allReleases = releases;
+      this.classesWithIllegalNames = classesWithIllegalNames;
+      AndroidRelease latestRelease = null;
+      AndroidRelease earliestUnreleased = null;
+      for (AndroidRelease release : allReleases) {
+        if (release.isReleased()) {
+          if (latestRelease == null || latestRelease.compareTo(release) > 0) {
+            latestRelease = release;
+          }
+        } else {
+          if (earliestUnreleased == null || earliestUnreleased.compareTo(release) < 0) {
+            earliestUnreleased = release;
+          }
+        }
+      }
+      this.latestRelease = latestRelease;
+      this.earliestUnreleased = earliestUnreleased;
+      verifyStaticInformation();
+    }
+
+    private void verifyStaticInformation() {
+      for (AndroidRelease release : this.allReleases) {
+        // Construct a map of all sdkInts to releases and note duplicates
+        AndroidRelease sdkCollision = this.sdkIntToAllReleases.put(release.getSdkInt(), release);
+        if (sdkCollision != null) {
+          this.sdkIntCollisions.add(new AbstractMap.SimpleEntry<>(release, sdkCollision));
+        }
+        // Construct a map of all short codes to releases, and note duplicates
+        this.shortCodeToAllReleases.put(release.getShortCode(), release);
+        // There is no need to check for shortCode duplicates as the Field name must match the
+        // short code.
+      }
+      if (earliestUnreleased != null
+          && latestRelease != null
+          && latestRelease.getSdkInt() >= earliestUnreleased.getSdkInt()) {
+        sdkApiMisordered = new AbstractMap.SimpleEntry<>(latestRelease, earliestUnreleased);
+      }
+    }
+
+    private void throwStaticErrors() {
+      StringBuilder errors = new StringBuilder();
+      if (!this.classesWithIllegalNames.isEmpty()) {
+        errors
+            .append("The following classes do not follow the naming criteria for ")
+            .append("releases or do not have the short codes in ")
+            .append("their internal fields. Please correct them: ")
+            .append(this.classesWithIllegalNames)
+            .append("\n");
+      }
+      if (sdkApiMisordered != null) {
+        errors
+            .append("The latest released sdk ")
+            .append(sdkApiMisordered.getKey().getShortCode())
+            .append(" has a sdkInt greater than the earliest unreleased sdk ")
+            .append(sdkApiMisordered.getValue().getShortCode())
+            .append("this implies sdks were released out of order which is highly unlikely.\n");
+      }
+      if (!sdkIntCollisions.isEmpty()) {
+        errors.append(
+            "The following sdks have different shortCodes, but identical sdkInt " + "versions:\n");
+        for (Map.Entry<AndroidRelease, AndroidRelease> entry : sdkIntCollisions) {
+          errors
+              .append("Both ")
+              .append(entry.getKey().getShortCode())
+              .append(" and ")
+              .append(entry.getValue().getShortCode())
+              .append("have the same sdkInt value of ")
+              .append(entry.getKey().getSdkInt())
+              .append("\n");
+        }
+      }
+      if (errors.length() > 0) {
+        throw new RuntimeException(
+            errors
+                .append("Please check the AndroidReleases defined ")
+                .append("in ")
+                .append(AndroidVersions.class.getName())
+                .append("and ensure they are aligned with the versions of")
+                .append(" Android.")
+                .toString());
+      }
+    }
+
+    public AndroidRelease computeCurrentSdk(
+        int reportedVersion, String releaseName, String codename, List<String> activeCodeNames) {
+      Logger.info("Reported Version: " + reportedVersion);
+      Logger.info("Release Name: " + releaseName);
+      Logger.info("Code Name: " + codename);
+      Logger.info("Active Code Names: " + String.join(",", activeCodeNames));
+
+      AndroidRelease current = null;
+      // Special case "REL", which means the build is not a pre-release build.
+      if ("REL".equals(codename)) {
+        // the first letter of the code name equal to the release number.
+        current = sdkIntToAllReleases.get(reportedVersion);
+        if (current != null && !current.isReleased()) {
+          throw new RuntimeException(
+              "The current sdk "
+                  + current.getShortCode()
+                  + " has been released. Please update the contents of "
+                  + AndroidVersions.class.getName()
+                  + " to mark sdk "
+                  + current.getShortCode()
+                  + " as released.");
+        }
+      } else {
+        // Get known active code name letters
+
+        List<String> activeCodenameLetter = new ArrayList<>();
+        for (String name : activeCodeNames) {
+          activeCodenameLetter.add(name.toUpperCase(Locale.getDefault()).substring(0, 1));
+        }
+
+        // If the process is operating with a code name.
+        if (codename != null) {
+          StringBuilder detectedProblems = new StringBuilder();
+          // This is safe for minor releases ( X.1 ) as long as they have added an entry
+          // corresponding to the sdk of that release and the prior major release is marked as
+          // "released" on its entry in this file.  If not this class will fail to initialize.
+          // The assumption is that only one of the major or minor version of a code name
+          // is under development and unreleased at any give time (S or Sv2).
+          String foundCode = codename.toUpperCase(Locale.getDefault()).substring(0, 1);
+          int loc = activeCodenameLetter.indexOf(foundCode);
+          if (loc == -1) {
+            detectedProblems
+                .append("The current codename's (")
+                .append(codename)
+                .append(") first letter (")
+                .append(foundCode)
+                .append(") is not in the list of active code's first letters: ")
+                .append(activeCodenameLetter)
+                .append("\n");
+          } else {
+            // attempt to find assume the fullname is the "shortCode", aka "Sv2", "OMR1".
+            current = shortCodeToAllReleases.get(codename);
+            // else, assume the fullname is the first letter is correct.
+            if (current == null) {
+              current = shortCodeToAllReleases.get(String.valueOf(foundCode));
+            }
+          }
+          if (current == null) {
+            detectedProblems
+                .append("No known release is associated with the shortCode of \"")
+                .append(foundCode)
+                .append("\" or \"")
+                .append(codename)
+                .append("\"\n");
+          } else if (current.isReleased()) {
+            detectedProblems
+                .append("The current sdk ")
+                .append(current.getShortCode())
+                .append(" has been been marked as released. Please update the ")
+                .append("contents of current sdk jar to the released version.\n");
+          }
+          if (detectedProblems.length() > 0) {
+            throw new RuntimeException(detectedProblems.toString());
+          }
+        }
+      }
+      return current;
+    }
+  }
+
+  /**
+   * Reads all AndroidReleases in this class and populates SdkInformation, checking for sanity in
+   * the shortCode, sdkInt, and release information.
+   *
+   * <p>All errors are stored and can be reported at once by asking the SdkInformation to throw a
+   * runtime exception after it has been populated.
+   */
+  static SdkInformation gatherStaticSdkInformationFromThisClass() {
+    List<AndroidRelease> allReleases = new ArrayList<>();
+    List<Class<? extends AndroidRelease>> classesWithIllegalNames = new ArrayList<>();
+    for (Class<?> clazz : AndroidVersions.class.getClasses()) {
+      if (AndroidRelease.class.isAssignableFrom(clazz)
+          && !clazz.isInterface()
+          && !Modifier.isAbstract(clazz.getModifiers())) {
+        try {
+          AndroidRelease rel = (AndroidRelease) clazz.getDeclaredConstructor().newInstance();
+          allReleases.add(rel);
+          // inspect field name - as this is our only chance to inspect it.
+          if (!rel.getClass().getSimpleName().equals(rel.getShortCode())) {
+            classesWithIllegalNames.add(rel.getClass());
+          }
+        } catch (NoSuchMethodException
+            | InstantiationException
+            | IllegalArgumentException
+            | IllegalAccessException
+            | InvocationTargetException ex) {
+          throw new RuntimeException(
+              "Classes "
+                  + clazz.getName()
+                  + "should be accessible via "
+                  + AndroidVersions.class.getCanonicalName()
+                  + " and have a default public no-op constructor ",
+              ex);
+        }
+      }
+    }
+    Collections.sort(allReleases, AndroidRelease::compareTo);
+
+    SdkInformation sdkInformation = new SdkInformation(allReleases, classesWithIllegalNames);
+    sdkInformation.throwStaticErrors();
+    return sdkInformation;
+  }
+
+  static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException {
+    ZipEntry buildProp = jarFile.getEntry("build.prop");
+    Properties buildProps = new Properties();
+    buildProps.load(jarFile.getInputStream(buildProp));
+    return computeCurrentSdkFromBuildProps(buildProps);
+  }
+
+  static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) {
+    // 33, 34, 35 ....
+    String sdkVersionString = buildProps.getProperty("ro.build.version.sdk");
+    int sdk = sdkVersionString == null ? 0 : Integer.parseInt(sdkVersionString);
+    // "REL"
+    String release = buildProps.getProperty("ro.build.version.release");
+    // "Tiramasu", "UpsideDownCake"
+    String codename = buildProps.getProperty("ro.build.version.codename");
+    // "Tiramasu,UpsideDownCake", "UpsideDownCake", "REL"
+    String codenames = buildProps.getProperty("ro.build.version.all_codenames");
+    String[] allCodeNames = codenames == null ? new String[0] : codenames.split(",");
+    String[] activeCodeNames =
+        allCodeNames.length > 0 && allCodeNames[0].equals("REL") ? new String[0] : allCodeNames;
+    return information.computeCurrentSdk(sdk, release, codename, asList(activeCodeNames));
+  }
+
+  /**
+   * If we are working in android source, this code detects the list of active code names if any.
+   */
+  private static List<String> getActiveCodeNamesIfAny(Class<?> targetClass) {
+    try {
+      Field activeCodeFields = targetClass.getDeclaredField("ACTIVE_CODENAMES");
+      String[] activeCodeNames = (String[]) activeCodeFields.get(null);
+      return asList(activeCodeNames);
+    } catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException ex) {
+      return new ArrayList<>();
+    }
+  }
+
+  private static final SdkInformation information;
+
+  static {
+    AndroidRelease currentRelease = null;
+    information = gatherStaticSdkInformationFromThisClass();
+    try {
+      Class<?> buildClass =
+          Class.forName("android.os.Build", false, Thread.currentThread().getContextClassLoader());
+      System.out.println("build class " + buildClass);
+      Class<?> versionClass = null;
+      for (Class<?> c : buildClass.getClasses()) {
+        if (c.getSimpleName().equals("VERSION")) {
+          versionClass = c;
+          System.out.println("Version class " + versionClass);
+          break;
+        }
+      }
+      if (versionClass != null) {
+        // 33, 34, etc....
+        int sdkInt = (int) ReflectionHelpers.getStaticField(versionClass, "SDK_INT");
+        // Either unset, or 13, 14, etc....
+        String release = ReflectionHelpers.getStaticField(versionClass, "RELEASE");
+        // Either REL if release is set, or Tiramasu, UpsideDownCake, etc
+        String codename = ReflectionHelpers.getStaticField(versionClass, "CODENAME");
+        List<String> activeCodeNames = getActiveCodeNamesIfAny(versionClass);
+        currentRelease = information.computeCurrentSdk(sdkInt, release, codename, activeCodeNames);
+      }
+    } catch (ClassNotFoundException | IllegalArgumentException | UnsatisfiedLinkError e) {
+      // No op, this class should be usable outside of a Robolectric sandbox.
+    }
+    CURRENT = currentRelease;
+  }
+}
diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java
new file mode 100644
index 0000000..95b2c42
--- /dev/null
+++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsEdgeCaseTest.java
@@ -0,0 +1,71 @@
+package org.robolectric.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.robolectric.versioning.AndroidVersions.AndroidRelease;
+import org.robolectric.versioning.AndroidVersions.SdkInformation;
+
+/** Test more esoteric versions mismatches in sdkInt numbers, and codenames. */
+@RunWith(JUnit4.class)
+public final class AndroidVersionsEdgeCaseTest {
+
+  /**
+   * sdkInt higher than any known release, claims it's released. Expects an error message to update
+   * to update the AndroidVersions.class
+   */
+  @Test
+  public void sdkIntHigherThanKnownReleasesClaimsIsReleased_throwsException() {
+    AndroidRelease earliestUnrelease = null;
+    try {
+      SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass();
+      earliestUnrelease = information.earliestUnreleased;
+      information.computeCurrentSdk(
+          earliestUnrelease.getSdkInt(), earliestUnrelease.getVersion(), "REL", Arrays.asList());
+      assertThat(this).isNull();
+    } catch (RuntimeException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains(
+              "The current sdk "
+                  + earliestUnrelease.getShortCode()
+                  + " has been released. Please update the contents of "
+                  + AndroidVersions.class.getName()
+                  + " to mark sdk "
+                  + earliestUnrelease.getShortCode()
+                  + " as released.");
+      assertThat(e).isInstanceOf(RuntimeException.class);
+    }
+  }
+
+  /**
+   * sdkInt lower than known release, claims it's released. Expects an error message to update the
+   * jar.
+   */
+  @Test
+  public void sdkIntReleasedButStillReportsCodeName_throwsException() {
+    AndroidRelease latestRelease = null;
+    try {
+      SdkInformation information = AndroidVersions.gatherStaticSdkInformationFromThisClass();
+      latestRelease = information.latestRelease;
+      information.computeCurrentSdk(
+          latestRelease.getSdkInt(),
+          null,
+          information.latestRelease.getShortCode(),
+          Arrays.asList(latestRelease.getShortCode()));
+      assertThat(this).isNull();
+    } catch (RuntimeException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .contains(
+              "The current sdk "
+                  + latestRelease.getShortCode()
+                  + " has been been marked as released. Please update the contents of current sdk"
+                  + " jar to the released version.");
+      assertThat(e).isInstanceOf(RuntimeException.class);
+    }
+  }
+}
diff --git a/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java
new file mode 100644
index 0000000..dba93ca
--- /dev/null
+++ b/shadows/versioning/src/test/java/org/robolectric/versioning/AndroidVersionsTest.java
@@ -0,0 +1,205 @@
+package org.robolectric.versioning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build.VERSION;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.versioning.AndroidVersions.T;
+
+/**
+ * Check versions information aligns with runtime information. Primarily, selected SDK with
+ * internally detected version number.
+ */
+@RunWith(RobolectricTestRunner.class)
+public final class AndroidVersionsTest {
+
+  @Test
+  @Config(sdk = T.SDK_INT)
+  public void testStandardInitializationT() {
+    assertThat(VERSION.SDK_INT).isEqualTo(33);
+    assertThat(VERSION.RELEASE).isEqualTo("13");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.T.SHORT_CODE).isEqualTo("T");
+    assertThat(new AndroidVersions.T().getVersion()).isEqualTo("13.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("T");
+  }
+
+  @Test
+  @Config(sdk = 32)
+  public void testStandardInitializationSv2() {
+    assertThat(VERSION.SDK_INT).isEqualTo(32);
+    assertThat(VERSION.RELEASE).isEqualTo("12");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.Sv2.SHORT_CODE).isEqualTo("Sv2");
+    assertThat(new AndroidVersions.Sv2().getVersion()).isEqualTo("12.1");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Sv2");
+  }
+
+  @Test
+  @Config(sdk = 31)
+  public void testStandardInitializationS() {
+    assertThat(VERSION.SDK_INT).isEqualTo(31);
+    assertThat(VERSION.RELEASE).isEqualTo("12");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.S.SHORT_CODE).isEqualTo("S");
+    assertThat(new AndroidVersions.S().getVersion()).isEqualTo("12.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("S");
+  }
+
+  @Test
+  @Config(sdk = 30)
+  public void testStandardInitializationR() {
+    assertThat(VERSION.SDK_INT).isEqualTo(30);
+    assertThat(VERSION.RELEASE).isEqualTo("11");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.R.SHORT_CODE).isEqualTo("R");
+    assertThat(new AndroidVersions.R().getVersion()).isEqualTo("11.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("R");
+  }
+
+  @Test
+  @Config(sdk = 29)
+  public void testStandardInitializationQ() {
+    assertThat(VERSION.SDK_INT).isEqualTo(29);
+    assertThat(VERSION.RELEASE).isEqualTo("10");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.Q.SHORT_CODE).isEqualTo("Q");
+    assertThat(new AndroidVersions.Q().getVersion()).isEqualTo("10.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("Q");
+  }
+
+  @Test
+  @Config(sdk = 28)
+  public void testStandardInitializationP() {
+    assertThat(VERSION.SDK_INT).isEqualTo(28);
+    assertThat(VERSION.RELEASE).isEqualTo("9");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.P.SHORT_CODE).isEqualTo("P");
+    assertThat(new AndroidVersions.P().getVersion()).isEqualTo("9.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("P");
+  }
+
+  @Test
+  @Config(sdk = 27)
+  public void testStandardInitializationOMR1() {
+    assertThat(VERSION.SDK_INT).isEqualTo(27);
+    assertThat(VERSION.RELEASE).isEqualTo("8.1.0");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.OMR1.SHORT_CODE).isEqualTo("OMR1");
+    assertThat(new AndroidVersions.OMR1().getVersion()).isEqualTo("8.1");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("OMR1");
+  }
+
+  @Test
+  @Config(sdk = 26)
+  public void testStandardInitializationO() {
+    assertThat(VERSION.SDK_INT).isEqualTo(26);
+    assertThat(VERSION.RELEASE).isEqualTo("8.0.0");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.O.SHORT_CODE).isEqualTo("O");
+    assertThat(new AndroidVersions.O().getVersion()).isEqualTo("8.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("O");
+  }
+
+  @Test
+  @Config(sdk = 25)
+  public void testStandardInitializationNMR1() {
+    assertThat(VERSION.SDK_INT).isEqualTo(25);
+    assertThat(VERSION.RELEASE).isEqualTo("7.1");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.NMR1.SHORT_CODE).isEqualTo("NMR1");
+    assertThat(new AndroidVersions.NMR1().getVersion()).isEqualTo("7.1");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("NMR1");
+  }
+
+  @Test
+  @Config(sdk = 24)
+  public void testStandardInitializationN() {
+    assertThat(VERSION.SDK_INT).isEqualTo(24);
+    assertThat(VERSION.RELEASE).isEqualTo("7.0");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.N.SHORT_CODE).isEqualTo("N");
+    assertThat(new AndroidVersions.N().getVersion()).isEqualTo("7.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("N");
+  }
+
+  @Test
+  @Config(sdk = 23)
+  public void testStandardInitializationM() {
+    assertThat(VERSION.SDK_INT).isEqualTo(23);
+    assertThat(VERSION.RELEASE).isEqualTo("6.0.1");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.M.SHORT_CODE).isEqualTo("M");
+    assertThat(new AndroidVersions.M().getVersion()).isEqualTo("6.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("M");
+  }
+
+  @Test
+  @Config(sdk = 22)
+  public void testStandardInitializationLMR1() {
+    assertThat(VERSION.SDK_INT).isEqualTo(22);
+    assertThat(VERSION.RELEASE).isEqualTo("5.1.1");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.LMR1.SHORT_CODE).isEqualTo("LMR1");
+    assertThat(new AndroidVersions.LMR1().getVersion()).isEqualTo("5.1");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("LMR1");
+  }
+
+  @Test
+  @Config(sdk = 21)
+  public void testStandardInitializationL() {
+    assertThat(VERSION.SDK_INT).isEqualTo(21);
+    assertThat(VERSION.RELEASE).isEqualTo("5.0.2");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.L.SHORT_CODE).isEqualTo("L");
+    assertThat(new AndroidVersions.L().getVersion()).isEqualTo("5.0");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("L");
+  }
+
+  @Test
+  @Config(sdk = 19)
+  public void testStandardInitializationK() {
+    assertThat(VERSION.SDK_INT).isEqualTo(19);
+    assertThat(VERSION.RELEASE).isEqualTo("4.4");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.K.SHORT_CODE).isEqualTo("K");
+    assertThat(new AndroidVersions.K().getVersion()).isEqualTo("4.4");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("K");
+  }
+
+  @Test
+  @Config(sdk = 18)
+  public void testStandardInitializationJMR2() {
+    assertThat(VERSION.SDK_INT).isEqualTo(18);
+    assertThat(VERSION.RELEASE).isEqualTo("4.3");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.JMR2.SHORT_CODE).isEqualTo("JMR2");
+    assertThat(new AndroidVersions.JMR2().getVersion()).isEqualTo("4.3");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR2");
+  }
+
+  @Test
+  @Config(sdk = 17)
+  public void testStandardInitializationJMR1() {
+    assertThat(VERSION.SDK_INT).isEqualTo(17);
+    assertThat(VERSION.RELEASE).isEqualTo("4.2.2");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.JMR1.SHORT_CODE).isEqualTo("JMR1");
+    assertThat(new AndroidVersions.JMR1().getVersion()).isEqualTo("4.2");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("JMR1");
+  }
+
+  @Test
+  @Config(sdk = 16)
+  public void testStandardInitializationJ() {
+    assertThat(VERSION.SDK_INT).isEqualTo(16);
+    assertThat(VERSION.RELEASE).isEqualTo("4.1.2");
+    assertThat(VERSION.CODENAME).isEqualTo("REL");
+    assertThat(AndroidVersions.J.SHORT_CODE).isEqualTo("J");
+    assertThat(new AndroidVersions.J().getVersion()).isEqualTo("4.1");
+    assertThat(AndroidVersions.CURRENT.getShortCode()).isEqualTo("J");
+  }
+}
diff --git a/shadows/versioning/src/test/resources/AndroidManifest.xml b/shadows/versioning/src/test/resources/AndroidManifest.xml
new file mode 100644
index 0000000..65383ac
--- /dev/null
+++ b/shadows/versioning/src/test/resources/AndroidManifest.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.robolectric">
+    <uses-sdk android:targetSdkVersion="33" android:minSdkVersion="33"/>
+    <application android:name="android.app.Application">
+    </application>
+</manifest>
diff --git a/testapp/build.gradle b/testapp/build.gradle
index 651ced0..0abf895 100644
--- a/testapp/build.gradle
+++ b/testapp/build.gradle
@@ -2,6 +2,7 @@
 
 android {
     compileSdk 33
+    namespace 'org.robolectric.testapp'
 
     defaultConfig {
         minSdk 16
diff --git a/utils/build.gradle b/utils/build.gradle
index c10cca2..c31c9a0 100644
--- a/utils/build.gradle
+++ b/utils/build.gradle
@@ -1,3 +1,4 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
 import org.robolectric.gradle.DeployedRoboJavaModulePlugin
 import org.robolectric.gradle.RoboJavaModulePlugin
 
@@ -13,7 +14,7 @@
     }
 }
 
-tasks.withType(GenerateModuleMetadata) {
+tasks.withType(GenerateModuleMetadata).configureEach {
     // We don't want to release gradle module metadata now to avoid
     // potential compatibility problems.
     enabled = false
@@ -26,7 +27,7 @@
     // in production. If utils module starts to add Kotlin code in main source
     // set, we can remove this destinationDirectory modification.
     destinationDirectory = file("${projectDir}/build/classes/java/main")
-    compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
+    compilerOptions.jvmTarget = JvmTarget.JVM_1_8
 }
 
 afterEvaluate {
@@ -48,20 +49,19 @@
 dependencies {
     api project(":annotations")
     api project(":pluginapi")
-    api "javax.inject:javax.inject:1"
-    api "javax.annotation:javax.annotation-api:1.3.2"
+    api libs.javax.inject
+    api libs.javax.annotation.api
 
     // For @VisibleForTesting and ByteStreams
-    implementation "com.google.guava:guava:$guavaJREVersion"
-    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
+    implementation libs.guava
+    compileOnly libs.findbugs.jsr305
 
-    testCompileOnly "com.google.auto.service:auto-service-annotations:$autoServiceVersion"
-    testAnnotationProcessor "com.google.auto.service:auto-service:$autoServiceVersion"
-    testAnnotationProcessor "com.google.errorprone:error_prone_core:$errorproneVersion"
-    implementation "com.google.errorprone:error_prone_annotations:$errorproneVersion"
+    testCompileOnly libs.auto.service.annotations
+    testAnnotationProcessor libs.auto.service
+    testAnnotationProcessor libs.error.prone.core
+    implementation libs.error.prone.annotations
 
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
-    testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
-    testImplementation "org.mockito:mockito-core:${mockitoVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
+    testImplementation libs.kotlin.stdlib
 }
diff --git a/utils/reflector/build.gradle b/utils/reflector/build.gradle
index 3027345..140e987 100644
--- a/utils/reflector/build.gradle
+++ b/utils/reflector/build.gradle
@@ -5,12 +5,12 @@
 apply plugin: DeployedRoboJavaModulePlugin
 
 dependencies {
-    api "org.ow2.asm:asm:${asmVersion}"
-    api "org.ow2.asm:asm-commons:${asmVersion}"
-    api "org.ow2.asm:asm-util:${asmVersion}"
+    api libs.asm
+    api libs.asm.commons
+    api libs.asm.util
     api project(":utils")
 
     testImplementation project(":shadowapi")
-    testImplementation "junit:junit:${junitVersion}"
-    testImplementation "com.google.truth:truth:${truthVersion}"
+    testImplementation libs.junit4
+    testImplementation libs.truth
 }
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
new file mode 100644
index 0000000..d69c391
--- /dev/null
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Constructor.java
@@ -0,0 +1,11 @@
+package org.robolectric.util.reflector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Indicates that the annotated method is a constructor. */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Constructor {}
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
index 2873e28..12f855f 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/Reflector.java
@@ -38,6 +38,8 @@
   private static final boolean DEBUG = false;
   private static final AtomicInteger COUNTER = new AtomicInteger();
   private static final Map<Class<?>, Constructor<?>> cache = new ConcurrentHashMap<>();
+  private static final Map<Class<?>, Object> staticReflectorCache = new ConcurrentHashMap<>();
+
   /**
    * Returns an object which provides accessors for invoking otherwise inaccessible static methods
    * and fields.
@@ -56,6 +58,10 @@
    * @param target the target object
    */
   public static <T> T reflector(Class<T> iClass, Object target) {
+    if (target == null && staticReflectorCache.containsKey(iClass)) {
+      return (T) staticReflectorCache.get(iClass);
+    }
+
     Class<?> targetClass = determineTargetClass(iClass);
 
     Constructor<? extends T> ctor = (Constructor<? extends T>) cache.get(iClass);
@@ -68,11 +74,15 @@
                     () -> Reflector.<T>createReflectorClass(iClass, targetClass));
         ctor = reflectorClass.getConstructor(targetClass);
         ctor.setAccessible(true);
+        cache.put(iClass, ctor);
       }
 
-      cache.put(iClass, ctor);
+      T instance = ctor.newInstance(target);
+      if (target == null) {
+        staticReflectorCache.put(iClass, instance);
+      }
+      return instance;
 
-      return ctor.newInstance(target);
     } catch (NoSuchMethodException
         | InstantiationException
         | IllegalAccessException
diff --git a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
index d3e3668..ea9b45c 100644
--- a/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
+++ b/utils/reflector/src/main/java/org/robolectric/util/reflector/ReflectorClassWriter.java
@@ -15,6 +15,7 @@
 import java.lang.reflect.Modifier;
 import java.util.HashSet;
 import java.util.Set;
+import javax.annotation.Nullable;
 import org.objectweb.asm.ClassWriter;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
@@ -29,6 +30,8 @@
   private static final Type CLASS_TYPE = Type.getType(Class.class);
   private static final Type FIELD_TYPE = Type.getType(Field.class);
   private static final Type METHOD_TYPE = Type.getType(Method.class);
+  private static final Type CONSTRUCTOR_TYPE = Type.getType(java.lang.reflect.Constructor.class);
+
   private static final Type STRING_TYPE = Type.getType(String.class);
   private static final Type STRINGBUILDER_TYPE = Type.getType(StringBuilder.class);
 
@@ -45,6 +48,8 @@
       findMethod(Class.class, "getDeclaredField", new Class<?>[] {String.class});
   private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_METHOD =
       findMethod(Class.class, "getDeclaredMethod", new Class<?>[] {String.class, Class[].class});
+  private static final org.objectweb.asm.commons.Method CLASS$GET_DECLARED_CONSTRUCTOR =
+      findMethod(Class.class, "getDeclaredConstructor", new Class<?>[] {Class[].class});
   private static final org.objectweb.asm.commons.Method ACCESSIBLE_OBJECT$SET_ACCESSIBLE =
       findMethod(AccessibleObject.class, "setAccessible", new Class<?>[] {boolean.class});
   private static final org.objectweb.asm.commons.Method FIELD$GET =
@@ -53,6 +58,9 @@
       findMethod(Field.class, "set", new Class<?>[] {Object.class, Object.class});
   private static final org.objectweb.asm.commons.Method METHOD$INVOKE =
       findMethod(Method.class, "invoke", new Class<?>[] {Object.class, Object[].class});
+  private static final org.objectweb.asm.commons.Method CONSTRUCTOR$NEWINSTANCE =
+      findMethod(
+          java.lang.reflect.Constructor.class, "newInstance", new Class<?>[] {Object[].class});
   private static final org.objectweb.asm.commons.Method THROWABLE$GET_CAUSE =
       findMethod(Throwable.class, "getCause", new Class<?>[] {});
   private static final org.objectweb.asm.commons.Method OBJECT_INIT =
@@ -118,8 +126,11 @@
       if (method.isDefault()) continue;
 
       Accessor accessor = method.getAnnotation(Accessor.class);
+      Constructor constructor = method.getAnnotation(Constructor.class);
       if (accessor != null) {
         new AccessorMethodWriter(method, accessor).write();
+      } else if (constructor != null) {
+        new ConstructorMethodWriter(method).write();
       } else {
         new ReflectorMethodWriter(method).write();
       }
@@ -251,6 +262,135 @@
     }
   }
 
+  private class ConstructorMethodWriter extends BaseAdapter {
+
+    private final String constructorRefName;
+    private final Type[] targetParamTypes;
+
+    private ConstructorMethodWriter(Method method) {
+      super(method);
+      int myMethodNumber = nextMethodNumber++;
+      this.constructorRefName = "constructor" + myMethodNumber;
+      this.targetParamTypes = resolveParamTypes(iMethod);
+    }
+
+    void write() {
+      // write field to hold method reference...
+      visitField(
+          ACC_PRIVATE | ACC_STATIC,
+          constructorRefName,
+          CONSTRUCTOR_TYPE.getDescriptor(),
+          null,
+          null);
+
+      visitCode();
+
+      // pseudocode:
+      //   try {
+      //     return constructorN.newInstance(*args);
+      //   } catch (InvocationTargetException e) {
+      //     throw e.getCause();
+      //   } catch (ReflectiveOperationException e) {
+      //     throw new AssertionError("Error invoking reflector method in ClassLoader " +
+      // Instrumentation.class.getClassLoader(), e);
+      //   }
+      Label tryStart = new Label();
+      Label tryEnd = new Label();
+      Label handleInvocationTargetException = new Label();
+      visitTryCatchBlock(
+          tryStart,
+          tryEnd,
+          handleInvocationTargetException,
+          INVOCATION_TARGET_EXCEPTION_TYPE.getInternalName());
+      Label handleReflectiveOperationException = new Label();
+      visitTryCatchBlock(
+          tryStart,
+          tryEnd,
+          handleReflectiveOperationException,
+          REFLECTIVE_OPERATION_EXCEPTION_TYPE.getInternalName());
+
+      mark(tryStart);
+      loadOriginalConstructorRef();
+      loadArgArray();
+      invokeVirtual(CONSTRUCTOR_TYPE, CONSTRUCTOR$NEWINSTANCE);
+      mark(tryEnd);
+
+      castForReturn(iMethod.getReturnType());
+      returnValue();
+
+      mark(handleInvocationTargetException);
+
+      int exceptionLocalVar = newLocal(THROWABLE_TYPE);
+      storeLocal(exceptionLocalVar);
+      loadLocal(exceptionLocalVar);
+      invokeVirtual(THROWABLE_TYPE, THROWABLE$GET_CAUSE);
+      throwException();
+      mark(handleReflectiveOperationException);
+      exceptionLocalVar = newLocal(REFLECTIVE_OPERATION_EXCEPTION_TYPE);
+      storeLocal(exceptionLocalVar);
+      newInstance(STRINGBUILDER_TYPE);
+      dup();
+      invokeConstructor(STRINGBUILDER_TYPE, OBJECT_INIT);
+      push("Error invoking reflector method in ClassLoader ");
+      invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+      push(targetType);
+      invokeVirtual(CLASS_TYPE, CLASS$GET_CLASS_LOADER);
+      invokeStatic(STRING_TYPE, STRING$VALUE_OF);
+      invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$APPEND);
+      invokeVirtual(STRINGBUILDER_TYPE, STRINGBUILDER$TO_STRING);
+      int messageLocalVar = newLocal(STRING_TYPE);
+      storeLocal(messageLocalVar);
+      newInstance(ASSERTION_ERROR_TYPE);
+      dup();
+      loadLocal(messageLocalVar);
+      loadLocal(exceptionLocalVar);
+      invokeConstructor(ASSERTION_ERROR_TYPE, ASSERTION_ERROR_INIT);
+      throwException();
+
+      endMethod();
+    }
+
+    private void loadOriginalConstructorRef() {
+      // pseudocode:
+      //   if (constructorN == null) {
+      //     constructorN = targetClass.getDeclaredConstructor(paramTypes);
+      //     constructorN.setAccessible(true);
+      //   }
+      // -> constructor reference on stack
+      getStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+      dup();
+      Label haveConstructorRef = newLabel();
+      ifNonNull(haveConstructorRef);
+      pop();
+
+      // pseudocode:
+      //   targetClass.getDeclaredConstructor(paramTypes);
+      push(targetType);
+      Type[] paramTypes = targetParamTypes;
+      push(paramTypes.length);
+      newArray(CLASS_TYPE);
+      for (int i = 0; i < paramTypes.length; i++) {
+        dup();
+        push(i);
+        push(paramTypes[i]);
+        arrayStore(CLASS_TYPE);
+      }
+      invokeVirtual(CLASS_TYPE, CLASS$GET_DECLARED_CONSTRUCTOR);
+
+      // pseudocode:
+      //   <constructor>.setAccessible(true);
+      dup();
+      push(true);
+      invokeVirtual(CONSTRUCTOR_TYPE, ACCESSIBLE_OBJECT$SET_ACCESSIBLE);
+
+      // pseudocode:
+      //   constructorN = constructor;
+      dup();
+      putStatic(reflectorType, constructorRefName, CONSTRUCTOR_TYPE);
+      mark(haveConstructorRef);
+    }
+  }
+
   private class ReflectorMethodWriter extends BaseAdapter {
 
     private final String methodRefName;
@@ -375,35 +515,6 @@
       putStatic(reflectorType, methodRefName, METHOD_TYPE);
       mark(haveMethodRef);
     }
-
-    private Type[] resolveParamTypes(Method iMethod) {
-      Class<?>[] iParamTypes = iMethod.getParameterTypes();
-      Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
-
-      Type[] targetParamTypes = new Type[iParamTypes.length];
-      for (int i = 0; i < iParamTypes.length; i++) {
-        Class<?> paramType = findWithType(paramAnnotations[i]);
-        if (paramType == null) {
-          paramType = iParamTypes[i];
-        }
-        targetParamTypes[i] = Type.getType(paramType);
-      }
-      return targetParamTypes;
-    }
-
-    private Class<?> findWithType(Annotation[] paramAnnotation) {
-      for (Annotation annotation : paramAnnotation) {
-        if (annotation instanceof WithType) {
-          String withTypeName = ((WithType) annotation).value();
-          try {
-            return Class.forName(withTypeName, true, iClass.getClassLoader());
-          } catch (ClassNotFoundException e1) {
-            // it's okay, ignore
-          }
-        }
-      }
-      return null;
-    }
   }
 
   private static String[] getInternalNames(final Class<?>[] types) {
@@ -494,5 +605,35 @@
     void loadNull() {
       visitInsn(Opcodes.ACONST_NULL);
     }
+
+    protected Type[] resolveParamTypes(Method iMethod) {
+      Class<?>[] iParamTypes = iMethod.getParameterTypes();
+      Annotation[][] paramAnnotations = iMethod.getParameterAnnotations();
+
+      Type[] targetParamTypes = new Type[iParamTypes.length];
+      for (int i = 0; i < iParamTypes.length; i++) {
+        Class<?> paramType = findWithType(paramAnnotations[i]);
+        if (paramType == null) {
+          paramType = iParamTypes[i];
+        }
+        targetParamTypes[i] = Type.getType(paramType);
+      }
+      return targetParamTypes;
+    }
+
+    @Nullable
+    private Class<?> findWithType(Annotation[] paramAnnotation) {
+      for (Annotation annotation : paramAnnotation) {
+        if (annotation instanceof WithType) {
+          String withTypeName = ((WithType) annotation).value();
+          try {
+            return Class.forName(withTypeName, true, iClass.getClassLoader());
+          } catch (ClassNotFoundException e1) {
+            // it's okay, ignore
+          }
+        }
+      }
+      return null;
+    }
   }
 }
diff --git a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
index 8baf3d6..74dc884 100644
--- a/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
+++ b/utils/reflector/src/test/java/org/robolectric/util/reflector/ReflectorTest.java
@@ -133,6 +133,25 @@
     time("saved accessor", 10_000_000, () -> fieldBySavedReflector(accessor));
   }
 
+  @Ignore
+  @Test
+  public void constructorPerf() {
+    SomeClass i = new SomeClass("c");
+
+    System.out.println("reflection = " + Collections.singletonList(methodByReflectionHelpers(i)));
+    System.out.println("accessor = " + Collections.singletonList(methodByReflector(i)));
+
+    _SomeClass_ accessor = reflector(_SomeClass_.class, i);
+
+    time("ReflectionHelpers", 10_000_000, this::constructorByReflectionHelpers);
+    time("accessor", 10_000_000, () -> constructorByReflector());
+    time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+
+    time("ReflectionHelpers", 10_000_000, () -> constructorByReflectionHelpers());
+    time("accessor", 10_000_000, () -> constructorByReflector());
+    time("saved accessor", 10_000_000, () -> constructorBySavedReflector(accessor));
+  }
+
   @Test
   public void nonExistentMethod_throwsAssertionError() {
     SomeClass i = new SomeClass("c");
@@ -143,6 +162,11 @@
     assertThat(ex).hasCauseThat().isInstanceOf(NoSuchMethodException.class);
   }
 
+  @Test
+  public void reflector_constructor() {
+    assertThat(staticReflector.newSomeClass("sdfsdf")).isNotNull();
+  }
+
   //////////////////////
 
   /** Accessor interface for {@link SomeClass}'s internals. */
@@ -170,6 +194,9 @@
     @Accessor("mD")
     int getD();
 
+    @Constructor
+    SomeClass newSomeClass(String c);
+
     String someMethod(String a, String b);
 
     String nonExistentMethod(String a, String b, String c);
@@ -251,6 +278,20 @@
     return reflector.someMethod("a", "b");
   }
 
+  private SomeClass constructorByReflectionHelpers() {
+    return ReflectionHelpers.callConstructor(
+        SomeClass.class, ClassParameter.from(String.class, "a"));
+  }
+
+  private SomeClass constructorByReflector() {
+    _SomeClass_ accessor = reflector(_SomeClass_.class);
+    return accessor.newSomeClass("a");
+  }
+
+  private SomeClass constructorBySavedReflector(_SomeClass_ reflector) {
+    return reflector.newSomeClass("a");
+  }
+
   private String fieldByReflectionHelpers(SomeClass o) {
     ReflectionHelpers.setField(o, "c", "abc");
     return ReflectionHelpers.getField(o, "c");
diff --git a/utils/src/main/java/org/robolectric/util/Util.java b/utils/src/main/java/org/robolectric/util/Util.java
old mode 100755
new mode 100644