Merge "Upgrade to junit 4.13.2" into androidx-main
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index 9fe9169..76af94b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -275,12 +275,16 @@
      * one of the typed versions of this method instead, such as {@link #getPropertyString} or
      * {@link #getPropertyStringArray}.
      *
+     * <p>If the property was assigned as an empty array using one of the
+     * {@code Builder#setProperty} functions, this method will return an empty array. If no such
+     * property exists at all, this method returns {@code null}.
+     *
      * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-     *   <p>If the property has been set is an empty {@link GenericDocument}[] or {@code byte[][]},
-     *   this will return an empty {@link GenericDocument}[] or {@code byte[][]} respectively
-     *   starting in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in
-     *   earlier versions of Android. For other types it will always return an empty
-     *   array if the property has been set as an empty array.
+     *   <p>Note: If the property is an empty {@link GenericDocument}[] or {@code byte[][]},
+     *   this method will return a {@code null} value in versions of Android prior to
+     *   {@link android.os.Build.VERSION_CODES#TIRAMISU Android T}. Starting in Android T it will
+     *   return an empty array if the property has been set as an empty array, matching the
+     *   behavior of other property types.
      * -->
      *
      * @param path The path to look for.
@@ -745,6 +749,12 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
+     * <p>If the property has not been set via {@link Builder#setPropertyString}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyString} to an empty
+     * {@code String[]}, this method returns an empty {@code String[]}.
+     *
      * @param path The path to look for.
      * @return The {@code String[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
@@ -761,6 +771,12 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
+     * <p>If the property has not been set via {@link Builder#setPropertyLong}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyLong} to an empty
+     * {@code long[]}, this method returns an empty {@code long[]}.
+     *
      * @param path The path to look for.
      * @return The {@code long[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
@@ -777,6 +793,12 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
+     * <p>If the property has not been set via {@link Builder#setPropertyDouble}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyDouble} to an empty
+     * {@code double[]}, this method returns an empty {@code double[]}.
+     *
      * @param path The path to look for.
      * @return The {@code double[]} associated with the given path, or {@code null} if no value is
      * set or the value is of a different type.
@@ -793,6 +815,12 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
+     * <p>If the property has not been set via {@link Builder#setPropertyBoolean}, this method
+     * returns {@code null}.
+     *
+     * <p>If it has been set via {@link Builder#setPropertyBoolean} to an empty
+     * {@code boolean[]}, this method returns an empty {@code boolean[]}.
+     *
      * @param path The path to look for.
      * @return The {@code boolean[]} associated with the given path, or {@code null} if no value
      * is set or the value is of a different type.
@@ -809,9 +837,15 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
-     * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-     *   <p> If the property has been set via  @link Builder#setPropertyBytes} is an empty {@code
-     *   byte[][]}. This will return an empty {@code byte[][]} respectively starting in
+     * <p>If the property has not been set via {@link Builder#setPropertyBytes}, this method
+     * returns {@code null}.
+     *
+     * <!--@exportToFramework:ifJetpack()-->
+     *   <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+     *   this method returns an empty {@code byte[][]}.
+     * <!--@exportToFramework:else()
+     *   <p>If it has been set via {@link Builder#setPropertyBytes} to an empty {@code byte[][]},
+     *   this method returns an empty {@code byte[][]} starting in
      *   {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
      *   versions of Android.
      * -->
@@ -833,11 +867,17 @@
      *
      * <p>See {@link #getProperty} for a detailed description of the path syntax.
      *
-     * <!--@exportToFramework:ifJetpack()--><!--@exportToFramework:else()
-     *   <p> If the property has been set via {@link Builder#setPropertyDocument} is an empty
-     *   {@link GenericDocument}[], this will return an empty {@link GenericDocument}[] respectively
-     *   starting in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in
-     *   earlier versions of Android.
+     * <p>If the property has not been set via {@link Builder#setPropertyDocument}, this method
+     * returns {@code null}.
+     *
+     * <!--@exportToFramework:ifJetpack()-->
+     *   <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+     *   {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]}.
+     * <!--@exportToFramework:else()
+     *   <p>If it has been set via {@link Builder#setPropertyDocument} to an empty
+     *   {@code GenericDocument[]}, this method returns an empty {@code GenericDocument[]} starting
+     *   in {@link android.os.Build.VERSION_CODES#TIRAMISU Android T} and {@code null} in earlier
+     *   versions of Android.
      * -->
      *
      * @param path The path to look for.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt
index 4b54959..9f10512 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt
@@ -54,7 +54,9 @@
     // recorded
     val experimentalApiFile: File,
     // File where the library's public resources are recorded
-    val resourceFile: File
+    val resourceFile: File,
+    // Directory where native API files are stored
+    val nativeApiDirectory: File
 ) : Serializable {
 
     /**
@@ -93,7 +95,8 @@
                 removedApiFile = File(apiFileDir, "$PREFIX_REMOVED$baseName$EXTENSION"),
                 restrictedApiFile = File(apiFileDir, "$PREFIX_RESTRICTED$baseName$EXTENSION"),
                 experimentalApiFile = File(apiFileDir, "$PREFIX_EXPERIMENTAL$baseName$EXTENSION"),
-                resourceFile = File(apiFileDir, "$PREFIX_RESOURCE$baseName$EXTENSION")
+                resourceFile = File(apiFileDir, "$PREFIX_RESOURCE$baseName$EXTENSION"),
+                nativeApiDirectory = File(apiFileDir, NATIVE_API_DIRECTORY_NAME).resolve(baseName)
             )
         }
 
@@ -126,6 +129,11 @@
          * Prefix used for resource-type API files.
          */
         private const val PREFIX_RESOURCE = "res-"
+
+        /**
+         * Directory name for location of native API files
+         */
+        private const val NATIVE_API_DIRECTORY_NAME = "native"
     }
 }
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
index 2cdd25a..790c771 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt
@@ -17,12 +17,14 @@
 package androidx.build.checkapi
 
 import androidx.build.AndroidXExtension
+import androidx.build.LibraryType
 import androidx.build.Release
 import androidx.build.RunApiTasks
 import androidx.build.Version
 import androidx.build.getAndroidJar
 import androidx.build.isWriteVersionedApiFilesEnabled
 import androidx.build.java.JavaCompileInputs
+import androidx.build.libabigail.NativeApiTasks
 import androidx.build.metalava.MetalavaTasks
 import androidx.build.multiplatformExtension
 import androidx.build.resources.ResourceTasks
@@ -190,6 +192,14 @@
             builtApiLocation, outputApiLocations
         )
 
+        if (extension.type == LibraryType.PUBLISHED_NATIVE_LIBRARY) {
+            NativeApiTasks.setupProject(
+                project = project,
+                builtApiLocation = builtApiLocation.nativeApiDirectory,
+                outputApiLocations = outputApiLocations.map { it.nativeApiDirectory }
+            )
+        }
+
         if (config is LibraryApiTaskConfig) {
             ResourceTasks.setupProject(
                 project, Release.DEFAULT_PUBLISH_CONFIG,
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiCompatibilityTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiCompatibilityTask.kt
new file mode 100644
index 0000000..9c93853
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiCompatibilityTask.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+import androidx.build.OperatingSystem
+import androidx.build.getOperatingSystem
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFiles
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+import org.gradle.workers.WorkAction
+import org.gradle.workers.WorkParameters
+import org.gradle.workers.WorkerExecutionException
+import org.gradle.workers.WorkerExecutor
+import java.io.ByteArrayOutputStream
+import java.io.File
+import javax.inject.Inject
+
+/**
+ * Task which depends on [GenerateNativeApiTask] and compares the current native API from the build
+ * directory to that stored under /native-api using abidiff. Throws an [AbiDiffException] if the API
+ * has incompatible changes.
+ */
+abstract class CheckNativeApiCompatibilityTask : DefaultTask() {
+
+    @get:Inject
+    abstract val workerExecutor: WorkerExecutor
+
+    @get:Internal
+    abstract val artifactNames: ListProperty<String>
+
+    @get:Internal
+    abstract val builtApiLocation: Property<File>
+
+    @get:Internal
+    abstract val currentApiLocation: Property<File>
+
+    @get:Input
+    abstract val strict: Property<Boolean>
+
+    @InputFiles
+    fun getTaskInputs(): List<File> {
+        return getLocationsForArtifacts(
+            builtApiLocation.get(),
+            artifactNames.get()
+        )
+    }
+
+    @OutputFiles
+    fun getTaskOutputs(): List<File> {
+        return getLocationsForArtifacts(
+            currentApiLocation.get(),
+            artifactNames.get()
+        )
+    }
+
+    @TaskAction
+    fun exec() {
+        if (getOperatingSystem() != OperatingSystem.LINUX) {
+            project.logger.warn(
+                "Native API checking is currently not supported on non-linux devices"
+            )
+            return
+        }
+        val builtApiFiles = builtApiLocation.get().walk().toList()
+        val currentApiFiles = currentApiLocation.get().walk().toList()
+
+        // Unless this is the first time we've generated these files, a difference in the number of
+        // API files indicates that a library has been added / removed and the API has changed.
+        if (currentApiFiles.isNotEmpty() && builtApiFiles.size != currentApiFiles.size) {
+            throw AbiDiffException("Number of built artifacts has changed, expected " +
+                "${currentApiFiles.size} but was ${builtApiFiles.size}")
+        }
+        val workQueue = workerExecutor.processIsolation()
+        builtApiLocation.get().listFiles().forEach { archDir ->
+            archDir.listFiles().forEach { apiFile ->
+                workQueue.submit(AbiDiffWorkAction::class.java) { parameters ->
+                    // the current API file of the same name as the one in the built location
+                    parameters.pathToPreviousLib = currentApiLocation.get()
+                        .resolve(archDir.name)
+                        .resolve(apiFile.name)
+                        .toString()
+                    // the newly built API file we want to check
+                    parameters.pathToCurrentLib = apiFile.toString()
+                    // necessary to locate `abidiff`
+                    parameters.rootDir = project.rootDir.toString()
+                }
+            }
+        }
+        workQueue.await()
+        logger.info("Native API check succeeded")
+    }
+}
+
+class AbiDiffException(message: String) : WorkerExecutionException(message)
+
+interface AbiDiffParameters : WorkParameters {
+    var rootDir: String
+    var pathToPreviousLib: String
+    var pathToCurrentLib: String
+}
+
+/**
+ * The exit value from `abidiff` is an 8-bit field, the specific bits have meaning.The exit codes
+ * we are about are:
+ *
+ * 0000 (0) -> success
+ * 0001 (1) -> tool error
+ * 0010 (2) -> user error (bad flags etc)
+ * 0100 (4) -> ABI changed
+ * 1100 (12) -> ABI changed + incompatible changes
+ *
+ * Remaining bits unused for now, so we should indeed error if we encounter them until we know
+ * their meaning.
+ * https://sourceware.org/libabigail/manual/abidiff.html#return-values
+ */
+enum class AbiDiffExitCode(val value: Int) {
+    SUCCESS(0),
+    TOOL_ERROR(1),
+    USER_ERROR(2),
+    ABI_CHANGE(4),
+    ABI_INCOMPATIBLE_CHANGE(12),
+    UNKNOWN(-1);
+    companion object {
+        fun fromInt(value: Int): AbiDiffExitCode = values().find { it.value == value } ?: UNKNOWN
+    }
+}
+
+abstract class AbiDiffWorkAction @Inject constructor(
+    private val execOperations: ExecOperations
+) : WorkAction<AbiDiffParameters> {
+    override fun execute() {
+        val outputStream = ByteArrayOutputStream()
+        val result = execOperations.exec {
+            it.executable = LibabigailPaths.Linux.abidiffPath(parameters.rootDir)
+            it.args = listOf(
+                parameters.pathToPreviousLib,
+                parameters.pathToCurrentLib
+            )
+            it.standardOutput = outputStream
+            it.isIgnoreExitValue = true
+        }
+        outputStream.close()
+        val exitValue = result.exitValue
+        val output = outputStream.toString()
+        when (AbiDiffExitCode.fromInt(exitValue)) {
+            AbiDiffExitCode.ABI_INCOMPATIBLE_CHANGE -> {
+                throw AbiDiffException("Incompatible API changes found! Please make sure these " +
+                    "are intentional and if so update the API file by " +
+                    "running 'ignoreBreakingChangesAndUpdateNativeApi'\n\n$output")
+            }
+            AbiDiffExitCode.TOOL_ERROR,
+            AbiDiffExitCode.USER_ERROR,
+            AbiDiffExitCode.UNKNOWN -> {
+                throw AbiDiffException("Encountered an error while executing 'abidiff', " +
+                    "this is likely a bug.\n\n$output")
+            }
+            AbiDiffExitCode.ABI_CHANGE, // non breaking changes are okay
+            AbiDiffExitCode.SUCCESS -> Unit
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiEquivalenceTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiEquivalenceTask.kt
new file mode 100644
index 0000000..4c9fcaf
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/CheckNativeApiEquivalenceTask.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+import androidx.build.metalava.checkEqual
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Checks that the native API files in the build folder are exactly the same as the checked in
+ * native API files.
+ */
+abstract class CheckNativeApiEquivalenceTask : DefaultTask() {
+    /**
+     * Api file (in the build dir) to check
+     */
+    @get:Input
+    abstract val builtApi: Property<File>
+
+    /**
+     * Api file (in source control) to compare against
+     */
+    @get:Input
+    abstract val checkedInApis: ListProperty<File>
+
+    @get:Internal
+    abstract val artifactNames: ListProperty<String>
+
+    @InputFiles
+    fun getTaskInputs(): List<File> {
+        return getLocationsForArtifacts(
+            builtApi.get(),
+            artifactNames.get()
+        ) + checkedInApis.get().flatMap { checkedInApi ->
+            getLocationsForArtifacts(
+                checkedInApi,
+                artifactNames.get()
+            )
+        }
+    }
+
+    @TaskAction
+    fun exec() {
+        val builtApiLocation = builtApi.get()
+        for (checkedInApi in checkedInApis.get()) {
+            for (artifactName in artifactNames.get()) {
+                for (arch in architectures) {
+                    checkEqual(
+                        builtApiLocation.resolve("$arch/lib$artifactName.xml"),
+                        checkedInApi.resolve("$arch/lib$artifactName.xml")
+                    )
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/GenerateNativeApiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/GenerateNativeApiTask.kt
new file mode 100644
index 0000000..dc5cbda
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/GenerateNativeApiTask.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+import androidx.build.OperatingSystem
+import androidx.build.getOperatingSystem
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.ArchiveOperations
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFiles
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+import org.gradle.workers.WorkAction
+import org.gradle.workers.WorkParameters
+import org.gradle.workers.WorkerExecutor
+import java.io.File
+import javax.inject.Inject
+
+private const val ARCH_PREFIX = "android."
+internal val architectures = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
+
+/**
+ * Task which generates native APIs files for each library built by the 'buildCmakeDebug' task using
+ * `abidw` and stores them in the /native-api in the project build directory.
+ */
+abstract class GenerateNativeApiTask : DefaultTask() {
+
+    @get:Inject
+    abstract val workerExecutor: WorkerExecutor
+
+    @get:Inject
+    abstract val archiveOperations: ArchiveOperations
+
+    @get:InputDirectory
+    abstract val prefabDirectory: Property<File>
+
+    @get:Internal
+    abstract val apiLocation: Property<File>
+
+    @get:Internal
+    abstract val artifactNames: ListProperty<String>
+
+    @OutputFiles
+    fun getTaskOutputs(): List<File> {
+        return getLocationsForArtifacts(
+            apiLocation.get(),
+            artifactNames.get()
+        )
+    }
+
+    @TaskAction
+    fun exec() {
+        if (getOperatingSystem() != OperatingSystem.LINUX) {
+            project.logger.warn(
+                "Native API checking is currently not supported on non-linux devices"
+            )
+            return
+        }
+        val destinationDir = apiLocation.get()
+        if (!destinationDir.exists()) {
+            destinationDir.mkdirs()
+        } else {
+            destinationDir.deleteRecursively()
+            destinationDir.mkdirs()
+        }
+        val prefabDir = prefabDirectory.get()
+        val workQueue = workerExecutor.processIsolation()
+        artifactNames.get().forEach { moduleName ->
+            val module = prefabDir.resolve("modules/$moduleName/libs")
+            module.listFiles().forEach { archDir ->
+                val artifacts = archDir.listFiles().filter {
+                    // skip abi.json
+                    it.extension == "a" || it.extension == "so"
+                }
+                val nameCounts = artifacts.groupingBy { it.nameWithoutExtension }.eachCount()
+                nameCounts.forEach { (name, count) ->
+                    if (count > 1) {
+                        throw GradleException(
+                            "Found multiple artifacts in $archDir with name '$name'"
+                        )
+                    }
+                }
+                artifacts.forEach { artifact ->
+                    val arch = archDir.name.removePrefix(ARCH_PREFIX)
+                    val outputFilePath = getLocationForArtifact(
+                        destinationDir,
+                        arch,
+                        artifact.nameWithoutExtension
+                    )
+                    outputFilePath.parentFile.mkdirs()
+                    workQueue.submit(AbiDwWorkAction::class.java) { parameters ->
+                        parameters.rootDir = project.rootDir.toString()
+                        parameters.headersDirs = findHeaderDirs()
+                        parameters.pathToLib = artifact.canonicalPath
+                        parameters.outputFilePath = outputFilePath.toString()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun findHeaderDirs(): List<String> {
+        return project.projectDir.walk().filter {
+            it.isDirectory && it.name == "include"
+        }.map { it.toString() }.toList()
+    }
+}
+
+interface AbiDwParameters : WorkParameters {
+    var rootDir: String
+    var headersDirs: List<String>
+    var pathToLib: String
+    var outputFilePath: String
+}
+
+abstract class AbiDwWorkAction @Inject constructor(private val execOperations: ExecOperations) :
+    WorkAction<AbiDwParameters> {
+    override fun execute() {
+        val headerDirsArgs = parameters.headersDirs.flatMap { listOf("--headers-dir", it) }
+        val tempFile = File.createTempFile("abi", null)
+        execOperations.exec {
+            it.executable = LibabigailPaths.Linux.abidwPath(parameters.rootDir)
+            it.args = headerDirsArgs + listOf(
+                "--drop-private-types",
+                "--out-file",
+                tempFile.toString(),
+                parameters.pathToLib
+            )
+        }
+        execOperations.exec {
+            it.executable = LibabigailPaths.Linux.abitidyPath(parameters.rootDir)
+            it.args = listOf(
+                "--input",
+                tempFile.toString(),
+                "--output",
+                parameters.outputFilePath,
+                "--abort-on-untyped-symbols"
+            )
+        }
+    }
+}
+
+internal fun getLocationsForArtifacts(baseDir: File, artifactNames: List<String>): List<File> {
+    return artifactNames.flatMap { artifactName ->
+        architectures.map { arch ->
+            getLocationForArtifact(baseDir, arch, artifactName)
+        }
+    }
+}
+
+/**
+ * Takes an [archName] and [artifactName] and returns the location within the build folder where
+ * that artifacts xml representation should be stored.
+ */
+private fun getLocationForArtifact(baseDir: File, archName: String, artifactName: String): File =
+    baseDir.resolve("$archName/$artifactName.xml")
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/LibabigailPaths.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/LibabigailPaths.kt
new file mode 100644
index 0000000..12e9bbb
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/LibabigailPaths.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+/**
+ * Locations of libabigail libraries (`abidw`, `abidff`) relative to the root project path.
+ */
+object LibabigailPaths {
+    object Linux {
+        private fun basePath(rootDir: String) =
+            "$rootDir/../../prebuilts/fullsdk-linux/kernel-build-tools/linux-x86/bin"
+        fun abidwPath(rootDir: String) = "${basePath(rootDir)}/abidw"
+        fun abidiffPath(rootDir: String) = "${basePath(rootDir)}/abidiff"
+        fun abitidyPath(rootDir: String) = "${basePath(rootDir)}/abitidy"
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/NativeApiTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/NativeApiTasks.kt
new file mode 100644
index 0000000..a0b9746
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/NativeApiTasks.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+import androidx.build.addToBuildOnServer
+import androidx.build.addToCheckTask
+import androidx.build.checkapi.getRequiredCompatibilityApiLocation
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.Project
+import java.io.File
+
+/**
+ * Adds native API generation / updating / checking tasks to a project.
+ */
+object NativeApiTasks {
+    private const val apiGroup = "API"
+
+    fun setupProject(
+        project: Project,
+        builtApiLocation: File,
+        outputApiLocations: List<File>,
+    ) {
+        val artifactNames = project.extensions.getByType(
+            LibraryExtension::class.java
+        ).prefab.names.toList()
+
+        // Generates API files from source in the build directory
+        val generateNativeApi = project.tasks.register(
+            "generateNativeApi",
+            GenerateNativeApiTask::class.java
+        ) { task ->
+            task.group = apiGroup
+            task.description = "Generates API files from native source"
+            task.prefabDirectory.set(
+                project.buildDir.resolve("intermediates/prefab_package/release/prefab")
+            )
+            task.artifactNames.set(artifactNames)
+            task.apiLocation.set(builtApiLocation)
+            task.dependsOn("prefabReleasePackage")
+        }
+
+        // Checks that there are no breaking changes since the last (non alpha) release
+        val requiredCompatibilityApiLocation = project.getRequiredCompatibilityApiLocation()
+        val checkNativeApiRelease = requiredCompatibilityApiLocation?.let { lastReleasedApiFile ->
+            project.tasks.register(
+                "checkNativeApiRelease",
+                CheckNativeApiCompatibilityTask::class.java
+            ) { task ->
+                task.group = apiGroup
+                task.description = "Checks that the API generated from native sources is  " +
+                    "compatible with the last released API file"
+                task.artifactNames.set(artifactNames)
+                task.builtApiLocation.set(builtApiLocation)
+                task.currentApiLocation.set(lastReleasedApiFile.nativeApiDirectory)
+                // only check for breaking changes here
+                task.strict.set(false)
+                task.dependsOn(generateNativeApi)
+            }
+        }
+
+        // Checks that API present in source matches that of the current generated API files
+        val checkNativeApi =
+            project.tasks.register(
+                "checkNativeApi",
+                CheckNativeApiEquivalenceTask::class.java
+            ) { task ->
+                task.group = apiGroup
+                task.description = "Checks that the API generated from native sources matches " +
+                    "the checked in API file"
+                task.artifactNames.set(artifactNames)
+                task.builtApi.set(builtApiLocation)
+                task.checkedInApis.set(outputApiLocations)
+                // Even if our API files are up to date, we still want to make sure we haven't
+                // made any incompatible changes since last release
+                checkNativeApiRelease?.let { task.dependsOn(it) }
+                task.dependsOn(generateNativeApi)
+            }
+
+        // Update the native API files if there are no breaking changes since the last (non-alpha)
+        // release.
+        project.tasks.register("updateNativeApi", UpdateNativeApi::class.java) {
+                task ->
+            task.group = apiGroup
+            task.description = "Updates the checked in API files to match source code API"
+            task.artifactNames.set(artifactNames)
+            task.inputApiLocation.set(builtApiLocation)
+            task.outputApiLocations.set(outputApiLocations)
+            task.dependsOn(generateNativeApi)
+            // only allow updating the API files if there are no breaking changes from the last
+            // released version. If for whatever reason we want to ignore this,
+            // `ignoreBreakingChangesAndUpdateNativeApi` can be used.
+            checkNativeApiRelease?.let { task.dependsOn(it) }
+        }
+
+        // Identical to `updateNativeApi` but does not depend on `checkNativeApiRelease`
+        project.tasks.register(
+            "ignoreBreakingChangesAndUpdateNativeApi",
+            UpdateNativeApi::class.java
+        ) { task ->
+            task.group = apiGroup
+            task.description = "Updates the checked in API files to match source code API" +
+                "including breaking changes"
+            task.artifactNames.set(artifactNames)
+            task.inputApiLocation.set(builtApiLocation)
+            task.outputApiLocations.set(outputApiLocations)
+            task.dependsOn(generateNativeApi)
+        }
+
+        project.addToCheckTask(checkNativeApi)
+        project.addToBuildOnServer(checkNativeApi)
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/libabigail/UpdateNativeApi.kt b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/UpdateNativeApi.kt
new file mode 100644
index 0000000..88d6a38
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/libabigail/UpdateNativeApi.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.libabigail
+
+import androidx.build.OperatingSystem
+import androidx.build.getOperatingSystem
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputFiles
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+/**
+ * Task which depends on `[GenerateNativeApiTask] and takes the generated native API files from the
+ * build directory and copies them to the current /native-api directory.
+ */
+abstract class UpdateNativeApi : DefaultTask() {
+
+    @get:Internal
+    abstract val artifactNames: ListProperty<String>
+
+    @get:Internal
+    abstract val inputApiLocation: Property<File>
+
+    @get:Internal
+    abstract val outputApiLocations: ListProperty<File>
+
+    @InputFiles
+    fun getTaskInputs(): List<File> {
+        return getLocationsForArtifacts(
+            inputApiLocation.get(),
+            artifactNames.get()
+        )
+    }
+
+    @OutputFiles
+    fun getTaskOutputs(): List<File> {
+        return outputApiLocations.get().flatMap { outputApiLocation ->
+            getLocationsForArtifacts(
+                outputApiLocation,
+                artifactNames.get()
+            )
+        }
+    }
+
+    @TaskAction
+    fun exec() {
+        if (getOperatingSystem() != OperatingSystem.LINUX) {
+            project.logger.warn(
+                "Native API checking is currently not supported on non-linux devices"
+            )
+            return
+        }
+        outputApiLocations.get().forEach { dir ->
+            dir.listFiles()?.forEach {
+                it.delete()
+            }
+        }
+        outputApiLocations.get().forEach { outputLocation ->
+            inputApiLocation.get().copyRecursively(target = outputLocation, overwrite = true)
+        }
+    }
+}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index b5d5239..d8bf4cc 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -74,6 +74,11 @@
     INTERNAL_TEST_LIBRARY(
         checkApi = RunApiTasks.No("Internal Library")
     ),
+    PUBLISHED_NATIVE_LIBRARY(
+        publish = Publish.SNAPSHOT_AND_RELEASE,
+        sourceJars = true,
+        checkApi = RunApiTasks.Yes()
+    ),
     SAMPLES(
         publish = Publish.SNAPSHOT_AND_RELEASE,
         sourceJars = true,
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
index fe65573..239548e 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplCameraReopenTest.java
@@ -243,7 +243,9 @@
         // Try opening the camera. This will fail and trigger reopening attempts
         mCamera2CameraImpl.open();
         // make camera unavailable.
-        mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+        sCameraExecutor.execute(() -> {
+            mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+        });
         assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
                 TimeUnit.MILLISECONDS)).isTrue();
 
@@ -278,10 +280,12 @@
                 TimeUnit.MILLISECONDS)).isTrue();
 
         // Emulate camera is interrupted by other client.
-        mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
-        CameraDevice cameraDevice = mock(CameraDevice.class);
-        cameraManagerImpl.getDeviceStateCallback().onDisconnected(cameraDevice);
-        cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+        sCameraExecutor.execute(() -> {
+            mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+            CameraDevice cameraDevice = mock(CameraDevice.class);
+            cameraManagerImpl.getDeviceStateCallback().onDisconnected(cameraDevice);
+            cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+        });
 
         // Enable active resuming which will open the camera even when camera is unavailable.
         mCamera2CameraImpl.setActiveResumingMode(true);
@@ -309,11 +313,13 @@
 
         mCamera2CameraImpl.setActiveResumingMode(true);
         // make camera unavailable.
-        mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
-        CameraDevice cameraDevice = mock(CameraDevice.class);
-        cameraManagerImpl.getDeviceStateCallback().onError(cameraDevice,
-                CameraDevice.StateCallback.ERROR_CAMERA_DEVICE);
-        cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+        sCameraExecutor.execute(() -> {
+            mCamera2CameraImpl.getCameraAvailability().onCameraUnavailable(mCameraId);
+            CameraDevice cameraDevice = mock(CameraDevice.class);
+            cameraManagerImpl.getDeviceStateCallback().onError(cameraDevice,
+                    CameraDevice.StateCallback.ERROR_CAMERA_DEVICE);
+            cameraManagerImpl.getDeviceStateCallback().onClosed(cameraDevice);
+        });
 
         // 2nd camera open should not happen
         assertThat(cameraOpenSemaphore.tryAcquire(1, WAIT_FOR_CAMERA_OPEN_TIMEOUT_MS,
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index a207732..b2e8231 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -63,6 +63,7 @@
 import androidx.camera.core.impl.ImageOutputConfig;
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewNotCroppedByParentQuirk;
 import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
 import androidx.camera.view.transform.CoordinateTransform;
 import androidx.camera.view.transform.OutputTransform;
@@ -598,7 +599,8 @@
         // TODO(b/159127402): use TextureView if target rotation is not display rotation.
         boolean isLegacyDevice = surfaceRequest.getCamera().getCameraInfoInternal()
                 .getImplementationType().equals(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
-        boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null;
+        boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
+                ||  DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
         if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice
                 || hasSurfaceViewQuirk) {
             // Force to use TextureView when the device is running android 7.0 and below, legacy
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index 61b8959..23c305d 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -44,6 +44,10 @@
             quirks.add(new SurfaceViewStretchedQuirk());
         }
 
+        if (SurfaceViewNotCroppedByParentQuirk.load()) {
+            quirks.add(new SurfaceViewNotCroppedByParentQuirk());
+        }
+
         if (TextureViewRotationQuirk.load()) {
             quirks.add(new TextureViewRotationQuirk());
         }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirk.java
new file mode 100644
index 0000000..cd12519
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirk.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where a scaled up SurfaceView is not cropped by the parent View.
+ *
+ * <p> On certain Xiaomi devices, when the scale type is FILL_* and the preview is scaled up
+ * to be larger than its parent, the SurfaceView is not cropped by its parent. As the result, the
+ * preview incorrectly covers the neighboring UI elements. b/211370840
+ */
+public class SurfaceViewNotCroppedByParentQuirk implements Quirk {
+
+    private static final String XIAOMI = "XIAOMI";
+    private static final String RED_MI_NOTE_10_MODEL = "M2101K7AG";
+
+    static boolean load() {
+        return XIAOMI.equalsIgnoreCase(Build.MANUFACTURER)
+                && RED_MI_NOTE_10_MODEL.equalsIgnoreCase(Build.MODEL);
+    }
+
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
index 82c2e25..f67b783 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
@@ -26,6 +26,7 @@
 import androidx.camera.testing.fakes.FakeCamera;
 import androidx.camera.testing.fakes.FakeCameraInfoInternal;
 import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewNotCroppedByParentQuirk;
 import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
 
 import org.junit.After;
@@ -55,7 +56,7 @@
     }
 
     @Test
-    public void surfaceViewHasQuirk_useTextureView() {
+    public void surfaceViewStretchedQuirk_useTextureView() {
         // Arrange:
         QuirkInjector.inject(new SurfaceViewStretchedQuirk());
 
@@ -65,6 +66,17 @@
                 PreviewView.ImplementationMode.PERFORMANCE)).isTrue();
     }
 
+    @Test
+    public void surfaceViewNotCroppedQuirk_useTextureView() {
+        // Arrange:
+        QuirkInjector.inject(new SurfaceViewNotCroppedByParentQuirk());
+
+        // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
+        assertThat(PreviewView.shouldUseTextureView(
+                createSurfaceRequestCompatibleWithSurfaceView(),
+                PreviewView.ImplementationMode.PERFORMANCE)).isTrue();
+    }
+
     private SurfaceRequest createSurfaceRequestCompatibleWithSurfaceView() {
         FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
         cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirkTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirkTest.kt
new file mode 100644
index 0000000..e0f3019
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewNotCroppedByParentQuirkTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk
+
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+/**
+ * Instrument tests for [SurfaceViewNotCroppedByParentQuirk].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SurfaceViewNotCroppedByParentQuirkTest {
+
+    @Test
+    fun quirkExistsOnRedMiNote10() {
+        // Arrange.
+        ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "M2101K7AG")
+        ReflectionHelpers.setStaticField(Build::class.java, "MANUFACTURER", "Xiaomi")
+
+        // Act.
+        val quirk = DeviceQuirks.get(
+            SurfaceViewNotCroppedByParentQuirk::class.java
+        )
+
+        // Assert.
+        assertThat(quirk).isNotNull()
+    }
+}
\ No newline at end of file
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
index cac11e3..250b970 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/templates/PlaceListTemplateBrowseDemoScreen.java
@@ -52,6 +52,7 @@
 
     final LocationListenerCompat mLocationListener;
     final HandlerThread mLocationUpdateHandlerThread;
+    boolean mHasPermissionLocation;
 
     @Nullable
     private Location mCurrentLocation;
@@ -59,6 +60,11 @@
     public PlaceListTemplateBrowseDemoScreen(@NonNull CarContext carContext) {
         super(carContext);
 
+        mHasPermissionLocation = carContext.checkSelfPermission(ACCESS_FINE_LOCATION)
+                == PackageManager.PERMISSION_GRANTED
+                || carContext.checkSelfPermission(ACCESS_COARSE_LOCATION)
+                == PackageManager.PERMISSION_GRANTED;
+
         mLocationUpdateHandlerThread = new HandlerThread("LocationThread");
         mLocationListener = location -> {
             mCurrentLocation = location;
@@ -68,10 +74,11 @@
         getLifecycle().addObserver(new DefaultLifecycleObserver() {
             @Override
             public void onResume(@NonNull LifecycleOwner owner) {
-                if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION)
+                mHasPermissionLocation = carContext.checkSelfPermission(ACCESS_FINE_LOCATION)
                         == PackageManager.PERMISSION_GRANTED
                         || carContext.checkSelfPermission(ACCESS_COARSE_LOCATION)
-                        == PackageManager.PERMISSION_GRANTED) {
+                        == PackageManager.PERMISSION_GRANTED;
+                if (mHasPermissionLocation) {
                     LocationManager locationManager =
                             carContext.getSystemService(LocationManager.class);
                     locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER,
@@ -110,7 +117,7 @@
                         .build())
                 .setTitle("Place List Template Demo")
                 .setHeaderAction(Action.BACK)
-                .setCurrentLocationEnabled(true);
+                .setCurrentLocationEnabled(mHasPermissionLocation);
 
         if (mCurrentLocation != null) {
             builder.setAnchor(new Place.Builder(CarLocation.create(mCurrentLocation)).build());
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
index 0d83981..90c1bba 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyGridTest.kt
@@ -32,10 +32,12 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.list.scrollBy
 import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsProperties
@@ -51,6 +53,7 @@
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -964,5 +967,24 @@
             .assert(keyIsDefined(SemanticsProperties.VerticalScrollAxisRange))
     }
 
+    @Test
+    fun rtl() {
+        val gridWidth = 30
+        val gridWidthDp = with(rule.density) { gridWidth.toDp() }
+        rule.setContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                LazyVerticalGrid(GridCells.Fixed(3), Modifier.width(gridWidthDp)) {
+                    items(3) {
+                        Box(Modifier.height(1.dp).testTag("$it"))
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("0").assertLeftPositionInRootIsEqualTo(gridWidthDp * 2 / 3)
+        rule.onNodeWithTag("1").assertLeftPositionInRootIsEqualTo(gridWidthDp / 3)
+        rule.onNodeWithTag("2").assertLeftPositionInRootIsEqualTo(0.dp)
+    }
+
     // TODO: add tests for the cache logic
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 608824d..fb48fc1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -478,7 +478,7 @@
         layout(constraints.maxWidth, placeables.fastMaxOfOrNull { it.height }!!) {
             var x = 0
             placeables.fastForEach { placeable ->
-                placeable.place(x, 0)
+                placeable.placeRelative(x, 0)
                 x += placeable.width + spacing
             }
         }
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 1ad1555..326f2d2 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -121,9 +121,3 @@
     description = "Tree composition support for code generated by the Compose compiler plugin and corresponding public API"
     legacyDisableKotlinStrictApiMode = true
 }
-
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions {
-        incremental = false
-    }
-}
diff --git a/development/build_log_simplifier/message-flakes.ignore b/development/build_log_simplifier/message-flakes.ignore
index e33ab73..037d92f 100644
--- a/development/build_log_simplifier/message-flakes.ignore
+++ b/development/build_log_simplifier/message-flakes.ignore
@@ -114,3 +114,5 @@
 System\.logW: A resource was acquired at attached stack trace but never released\. See java\.io\.Closeable for information on avoiding resource leaks\.java\.lang\.Throwable: Explicit termination method 'release' not called
 # > Task :camera:camera-camera2-pipe-integration:kaptReleaseKotlin
 warning\: The following options were not recognized by any processor\: \'\[dagger\.fastInit\, kapt\.kotlin\.generated\, dagger\.fullBindingGraphValidation\]\'
+# > Task :checkNativeApi / :generateNativeApi / :updateNativeApi
+Native API checking is currently not supported on non-linux devices
\ No newline at end of file
diff --git a/development/project-creator/create_project.py b/development/project-creator/create_project.py
index ae4b191..1aa4d84 100755
--- a/development/project-creator/create_project.py
+++ b/development/project-creator/create_project.py
@@ -420,6 +420,8 @@
 
     # Populate the library type
     library_type = get_library_type(artifact_id)
+    if project_type == ProjectType.NATIVE and library_type == "PUBLISHED_LIBRARY":
+        library_type = "PUBLISHED_NATIVE_LIBRARY"
     sed("<LIBRARY_TYPE>", library_type, full_artifact_path + "/build.gradle")
 
     # Populate the YEAR
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d74412d..8bce553d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -141,7 +141,7 @@
 playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
 protobuf = { module = "com.google.protobuf:protobuf-java", version = "3.4.0" }
 protobufCompiler = { module = "com.google.protobuf:protoc", version = "3.10.0" }
-protobufGradlePluginz = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.8.16" }
+protobufGradlePluginz = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.8.18" }
 protobufLite = { module = "com.google.protobuf:protobuf-javalite", version = "3.10.0" }
 reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
 retrofit = { module = "com.squareup.retrofit2:retrofit", version = "2.7.2" }
diff --git a/room/benchmark/build.gradle b/room/benchmark/build.gradle
index b985390..c2f2aa9 100644
--- a/room/benchmark/build.gradle
+++ b/room/benchmark/build.gradle
@@ -37,4 +37,5 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(project(":internal-testutils-common"))
 }
diff --git a/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt b/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
index 861da78..e3552e1 100644
--- a/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
+++ b/room/benchmark/src/androidTest/java/androidx/room/benchmark/InvalidationTrackerBenchmark.kt
@@ -31,6 +31,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import androidx.testutils.generateAllEnumerations
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -103,19 +104,15 @@
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "sampleSize={0}, mode={1}")
-        fun data(): List<Array<Any>> {
-            return mutableListOf<Array<Any>>().apply {
-                arrayOf(
+        fun data(): List<Array<Any>> =
+            generateAllEnumerations(
+                listOf(100, 1000, 5000, 10000),
+                listOf(
                     Mode.MEASURE_INSERT,
                     Mode.MEASURE_DELETE,
                     Mode.MEASURE_INSERT_AND_DELETE
-                ).forEach { mode ->
-                    arrayOf(100, 1000, 5000, 10000).forEach { sampleSize ->
-                        add(arrayOf(sampleSize, mode))
-                    }
-                }
-            }
-        }
+                )
+            )
 
         private const val DB_NAME = "invalidation-benchmark-test"
     }
diff --git a/room/benchmark/src/androidTest/java/androidx/room/benchmark/RelationBenchmark.kt b/room/benchmark/src/androidTest/java/androidx/room/benchmark/RelationBenchmark.kt
index 1000c14..d9ec441 100644
--- a/room/benchmark/src/androidTest/java/androidx/room/benchmark/RelationBenchmark.kt
+++ b/room/benchmark/src/androidTest/java/androidx/room/benchmark/RelationBenchmark.kt
@@ -32,6 +32,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
+import androidx.testutils.generateAllEnumerations
 import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -89,11 +90,7 @@
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "parentSampleSize={0}, childSampleSize={1}")
-        fun data() = arrayOf(100, 500, 1000).flatMap { parentSampleSize ->
-            arrayOf(10).map { childSampleSize ->
-                arrayOf(parentSampleSize, childSampleSize)
-            }
-        }
+        fun data() = generateAllEnumerations(listOf(100, 500, 1000), listOf(10))
 
         private const val DB_NAME = "relation-benchmark-test"
     }
diff --git a/room/room-compiler-processing/build.gradle b/room/room-compiler-processing/build.gradle
index d63f0af..95225c5 100644
--- a/room/room-compiler-processing/build.gradle
+++ b/room/room-compiler-processing/build.gradle
@@ -42,6 +42,7 @@
     testImplementation(libs.jsr250)
     testImplementation(libs.ksp)
     testImplementation(project(":room:room-compiler-processing-testing"))
+    testImplementation(project(":internal-testutils-common"))
 }
 
 tasks.withType(KotlinCompile).configureEach {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index 4cc1736..f9e64b5 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -21,7 +21,7 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.compileFiles
-import androidx.room.compiler.processing.util.generateAllEnumerations
+import androidx.testutils.generateAllEnumerations
 import androidx.room.compiler.processing.util.javaTypeUtils
 import androidx.room.compiler.processing.util.runKaptTest
 import androidx.room.compiler.processing.util.runProcessorTest
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelper.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelper.kt
deleted file mode 100644
index 35161e7..0000000
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelper.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.compiler.processing.util
-
-/**
- * Used to generate all argument enumerations for Parameterized tests.
- * See [ParameterizedHelperTest] for usage.
- */
-fun generateAllEnumerations(vararg args: List<Any?>): List<Array<Any?>> =
-    when (args.size) {
-        0 -> emptyList()
-        1 -> args[0].map {
-            arrayOf(it)
-        }
-        else -> generateAllEnumerations(
-            *args.dropLast(1).toTypedArray()
-        ).flatMap { prev ->
-            args.last().map { arg ->
-                prev + arg
-            }
-        }
-    }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelperTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelperTest.kt
deleted file mode 100644
index a534f52..0000000
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/util/ParameterizedHelperTest.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.room.compiler.processing.util
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class ParameterizedHelperTest {
-    @Test
-    fun testEnumerations() {
-        assertThat(generateAllEnumerations()).isEmpty()
-
-        // Comparing List of Arrays doesn't work(https://github.com/google/truth/issues/928), so
-        // we're mapping it to List of Lists
-        assertThat(
-            generateAllEnumerations(
-                listOf(false, true)
-            ).map { it.toList() }).isEqualTo(
-            listOf(
-                listOf<Any>(false), listOf<Any>(true)
-            )
-        )
-        assertThat(generateAllEnumerations(listOf(false, true), listOf())).isEmpty()
-        assertThat(
-            generateAllEnumerations(
-                listOf(false, true),
-                listOf(false, true)
-            ).map { it.toList() }
-        ).isEqualTo(
-            listOf(
-                listOf(false, false), listOf(false, true),
-                listOf(true, false), listOf(true, true)
-            )
-        )
-        assertThat(
-            generateAllEnumerations(
-                listOf(false, true),
-                (0..2).toList(),
-                listOf("low", "hi")
-            ).map { it.toList() }
-        ).isEqualTo(
-            listOf(
-                listOf(false, 0, "low"),
-                listOf(false, 0, "hi"),
-                listOf(false, 1, "low"),
-                listOf(false, 1, "hi"),
-                listOf(false, 2, "low"),
-                listOf(false, 2, "hi"),
-                listOf(true, 0, "low"),
-                listOf(true, 0, "hi"),
-                listOf(true, 1, "low"),
-                listOf(true, 1, "hi"),
-                listOf(true, 2, "low"),
-                listOf(true, 2, "hi")
-            )
-        )
-    }
-}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 0da469d6..0d266f3 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -122,6 +122,7 @@
             dir: "${new File(project(":sqlite:sqlite").buildDir, "libJar")}",
             include : "*.jar"
     ))
+    testImplementation(project(":internal-testutils-common"))
 }
 
 def generateAntlrTask = task("generateAntlrGrammar", type: GenerateAntlrGrammar) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
index a8dc8dc..453ffd0 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/NullAwareTypeConverterStore.kt
@@ -52,6 +52,9 @@
      */
     private val knownColumnTypes: List<XType>
 ) : TypeConverterStore {
+    private val knownColumnTypeNames = knownColumnTypes.map {
+        it.typeName
+    }
     override val typeConverters = if (context.processingEnv.backend == Backend.KSP) {
         val processedConverters = typeConverters.toMutableList()
         // create copies for converters that receive non-null values
@@ -154,6 +157,11 @@
         return null
     }
 
+    private fun isColumnType(type: XType): Boolean {
+        // compare using type names to handle both null and non-null.
+        return knownColumnTypeNames.contains(type.typeName)
+    }
+
     private fun findConverterIntoStatementInternal(
         input: XType,
         columnTypes: List<XType>
@@ -162,7 +170,8 @@
         val queue = TypeConverterQueue(
             sourceType = input,
             // each converter is keyed on which type they will take us to
-            keyType = TypeConverter::to
+            keyType = TypeConverter::to,
+            isKnownColumnType = this::isColumnType
         )
 
         while (true) {
@@ -178,7 +187,8 @@
             columnTypes.forEach { columnType ->
                 if (columnType.isAssignableFrom(current.type)) {
                     queue.maybeEnqueue(
-                        current.appendConverter(
+                        prevEntry = current,
+                        converter = current.appendConverter(
                             UpCastTypeConverter(
                                 upCastFrom = current.type,
                                 upCastTo = columnType
@@ -188,7 +198,10 @@
                 }
             }
             getAllTypeConvertersFrom(current.type).forEach {
-                queue.maybeEnqueue(current.appendConverter(it))
+                queue.maybeEnqueue(
+                    prevEntry = current,
+                    converter = current.appendConverter(it)
+                )
             }
         }
         return null
@@ -237,7 +250,8 @@
             sourceType = output,
             // each converter is keyed on which type they receive as we are doing pathfinding
             // reverse here
-            keyType = TypeConverter::from
+            keyType = TypeConverter::from,
+            isKnownColumnType = this::isColumnType
         )
 
         while (true) {
@@ -253,7 +267,8 @@
             columnTypes.forEach { columnType ->
                 if (current.type.isAssignableFrom(columnType)) {
                     queue.maybeEnqueue(
-                        current.prependConverter(
+                        prevEntry = current,
+                        converter = current.prependConverter(
                             UpCastTypeConverter(
                                 upCastFrom = columnType,
                                 upCastTo = current.type
@@ -263,7 +278,10 @@
                 }
             }
             getAllTypeConvertersTo(current.type).forEach {
-                queue.maybeEnqueue(current.prependConverter(it))
+                queue.maybeEnqueue(
+                    prevEntry = current,
+                    converter = current.prependConverter(it)
+                )
             }
         }
         return null
@@ -348,6 +366,7 @@
      */
     private class TypeConverterQueue(
         sourceType: XType,
+        val isKnownColumnType: (XType) -> Boolean,
         val keyType: TypeConverter.() -> XType
     ) {
         // using insertion order as the tie breaker for reproducible builds.
@@ -361,7 +380,8 @@
             val typeConverterEntry = TypeConverterEntry(
                 tieBreakerPriority = insertionOrder++,
                 type = sourceType,
-                converter = null
+                converter = null,
+                convertsBetweenDbAndNonDbType = false
             )
             cheapestEntry[sourceType] = typeConverterEntry
             queue.add(typeConverterEntry)
@@ -384,14 +404,31 @@
          * or visited with a more expensive converter.
          */
         fun maybeEnqueue(
+            prevEntry: TypeConverterEntry,
             converter: TypeConverter
         ): Boolean {
             val keyType = converter.keyType()
+            val convertsBetweenDbAndNonDbType =
+                isKnownColumnType(converter.from) != isKnownColumnType(converter.to)
+            if (prevEntry.convertsBetweenDbAndNonDbType) {
+                // if previous entry converted from db type to user type (or vice versa), the new
+                // converter must also be converting between db type and non-db type.
+                if (!convertsBetweenDbAndNonDbType) {
+                    // prev entry already visited a column type, we cannot add any converters that
+                    // will visit a non-column type
+                    return false
+                }
+            }
             val existing = cheapestEntry[keyType]
             if (existing == null ||
                 (existing.converter != null && existing.converter.cost > converter.cost)
             ) {
-                val entry = TypeConverterEntry(insertionOrder++, keyType, converter)
+                val entry = TypeConverterEntry(
+                    tieBreakerPriority = insertionOrder++,
+                    type = keyType,
+                    converter = converter,
+                    convertsBetweenDbAndNonDbType = convertsBetweenDbAndNonDbType
+                )
                 cheapestEntry[keyType] = entry
                 queue.add(entry)
                 return true
@@ -404,7 +441,16 @@
         // when costs are equal, tieBreakerPriority is used
         val tieBreakerPriority: Int,
         val type: XType,
-        val converter: TypeConverter?
+        val converter: TypeConverter?,
+        /**
+         * If true, this entry converts between a column type and a non column type. Once a
+         * converter entry converts between a column type and user type, it can never go back. This
+         * is to ensure that we don't find converters between unrelated user types just because they
+         * both convert to the same database type.
+         * For instance, both TypeA and TypeB might be convertible to `String` to be persisted, yet,
+         * this doesn't mean TypeA can be converted into TypeB.
+         */
+        val convertsBetweenDbAndNonDbType: Boolean
     ) : Comparable<TypeConverterEntry> {
         override fun compareTo(other: TypeConverterEntry): Int {
             if (converter == null) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
index 654bb51..3cc910d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/NullabilityAwareTypeConverterStoreTest.kt
@@ -22,6 +22,7 @@
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
 import androidx.room.compiler.processing.util.compiler.compile
+import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.processor.Context.BooleanProcessorOptions.USE_NULL_AWARE_CONVERTER
 import androidx.room.processor.CustomConverterProcessor
@@ -123,7 +124,8 @@
             MyClass? to String!: nullableMyClassToNullableString / checkNotNull(String?)
             String! to MyClass!: (String! as String?) / nullableStringToNullableMyClass / checkNotNull(MyClass?)
             MyClass! to String!: (MyClass! as MyClass?) / nullableMyClassToNullableString / checkNotNull(String?)
-            """.trimIndent())
+            """.trimIndent()
+        )
     }
 
     @Test
@@ -144,7 +146,8 @@
             MyClass? to Cursor: nullableMyClassToNullableString
             Cursor to MyClass!: nullableStringToNullableMyClass / checkNotNull(MyClass?)
             MyClass! to Cursor: (MyClass! as MyClass?) / nullableMyClassToNullableString
-            """.trimIndent())
+            """.trimIndent()
+        )
     }
 
     @Test
@@ -671,6 +674,118 @@
     }
 
     /**
+     * Repro for b/206961709
+     * Often times, user will provide type converters that convert user type to database type.
+     * This does not mean that two types that can be converted into db types can be converted into
+     * each-other. e.g. if you can serialize TypeA and TypeB to String, it doesn't mean you can
+     * convert TypeA to TypeB.
+     */
+    @Test
+    fun dontAssumeUserTypesCanBeConvertedIntoEachOther() {
+        val converters = Source.kotlin(
+            "Converters.kt",
+            """
+            import androidx.room.*
+            class TypeA
+            class TypeB
+            object MyConverters {
+                @TypeConverter
+                fun nullableStringToTypeA(input: String?): TypeA { TODO() }
+                @TypeConverter
+                fun nullableTypeAToString(input: TypeA): String { TODO() }
+                @TypeConverter
+                fun nullableTypeBToNullableString(input: TypeB?): String? { TODO() }
+                @TypeConverter
+                fun nullableStringToNullableTypeB(input: String?): TypeB? { TODO() }
+            }
+        """.trimIndent()
+        )
+        runKspTest(
+            sources = listOf(converters),
+            options = mapOf(
+                USE_NULL_AWARE_CONVERTER.argName to "true"
+            )
+        ) { invocation ->
+            val store = invocation.createStore("MyConverters")
+            val aType = invocation.processingEnv.requireType("TypeA")
+            val bType = invocation.processingEnv.requireType("TypeB")
+            val stringType = invocation.processingEnv.requireType("java.lang.String")
+            assertThat(
+                store.findTypeConverter(
+                    aType,
+                    bType
+                )?.toSignature()
+            ).isNull()
+            assertThat(
+                store.findTypeConverter(
+                    bType,
+                    aType
+                )?.toSignature()
+            ).isNull()
+            assertThat(
+                store.findTypeConverter(
+                    input = bType.makeNonNullable(),
+                    output = stringType
+                )?.toSignature()
+            ).isEqualTo(
+                """
+                (TypeB! as TypeB?) / nullableTypeBToNullableString / checkNotNull(String?)
+                """.trimIndent()
+            )
+        }
+    }
+
+    @Test // 3P provided test case from https://issuetracker.google.com/issues/206961709#comment4
+    fun dontAssumeTypesCanBeConvertedUserCase() {
+        val source = Source.kotlin(
+            "Foo.kt", """
+            import androidx.room.*
+            import java.time.Instant
+            enum class Awesomeness {
+                AWESOME,
+                SUPER_DUPER_AWESOME,
+            }
+            @TypeConverters(
+                TimeConverter::class,
+                AwesomenessConverter::class,
+            )
+            class TimeConverter {
+                @TypeConverter
+                fun instantToValue(value: Instant?): String? { TODO() }
+
+                @TypeConverter
+                fun valueToInstant(value: String?): Instant? { TODO() }
+            }
+
+            class AwesomenessConverter {
+                @TypeConverter
+                fun awesomenessToValue(value: Awesomeness): String { TODO() }
+
+                @TypeConverter
+                fun valueToAwesomeness(value: String?): Awesomeness { TODO() }
+            }
+        """.trimIndent()
+        )
+        runKspTest(
+            sources = listOf(source)
+        ) { invocation ->
+            val store = invocation.createStore(
+                "TimeConverter", "AwesomenessConverter"
+            )
+            val instantType = invocation.processingEnv.requireType("java.time.Instant")
+            val stringType = invocation.processingEnv.requireType("java.lang.String")
+            assertThat(
+                store.findTypeConverter(
+                    input = instantType,
+                    output = stringType
+                )?.toSignature()
+            ).isEqualTo(
+                "(Instant! as Instant?) / instantToValue / checkNotNull(String?)"
+            )
+        }
+    }
+
+    /**
      * Collect results for conversion from String to our type
      */
     private fun collectStringConversionResults(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
index 4ddee69..4b6fbf2 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/Signatures.kt
@@ -33,7 +33,7 @@
 }
 
 fun XType.toSignature() =
-    "$typeName${nullability.toSignature()}".substringAfter("java.lang.")
+    "$typeName${nullability.toSignature()}".substringAfterLast(".")
 
 fun TypeConverter.toSignature(): String {
     return when (this) {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
index 20772ab..a8b459a 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseWriterTest.kt
@@ -22,6 +22,7 @@
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
 import androidx.room.testing.asTestInvocationHandler
+import androidx.testutils.generateAllEnumerations
 import loadTestSource
 import org.junit.Test
 import org.junit.experimental.runners.Enclosed
@@ -123,15 +124,10 @@
         companion object {
             @Parameterized.Parameters(name = "(maxStatementCount, valuesPerEntity)={0}")
             @JvmStatic
-            fun getParams(): List<Pair<Int, Int>> {
-                val result = arrayListOf<Pair<Int, Int>>()
-                arrayListOf(500, 1000, 3000).forEach { maxStatementCount ->
-                    arrayListOf(50, 100, 200).forEach { valuesPerEntity ->
-                        result.add(maxStatementCount to valuesPerEntity)
-                    }
+            fun getParams(): List<Pair<Int, Int>> =
+                generateAllEnumerations(listOf(500, 1000, 3000), listOf(50, 100, 200)).map {
+                    it[0] as Int to it[1] as Int
                 }
-                return result
-            }
         }
     }
 }
diff --git a/testutils/testutils-common/build.gradle b/testutils/testutils-common/build.gradle
index 19480c5..6090eaf 100644
--- a/testutils/testutils-common/build.gradle
+++ b/testutils/testutils-common/build.gradle
@@ -26,6 +26,9 @@
 dependencies {
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesAndroid)
+
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
 }
 
 // Allow usage of Kotlin's @OptIn.
diff --git a/testutils/testutils-common/src/main/java/androidx/testutils/ParameterizedHelper.kt b/testutils/testutils-common/src/main/java/androidx/testutils/ParameterizedHelper.kt
new file mode 100644
index 0000000..270b30c
--- /dev/null
+++ b/testutils/testutils-common/src/main/java/androidx/testutils/ParameterizedHelper.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.testutils
+
+/**
+ * Generate all argument enumerations for Parameterized tests. For example,
+ * `generateAllEnumerations(listOf(false, true), listOf(1, 2, 3))` would return:
+ *
+ * ```
+ * [
+ *   [false, 1],
+ *   [false, 2],
+ *   [false, 3],
+ *   [true, 1],
+ *   [true, 2],
+ *   [true, 3]
+ * ]
+ * ```
+ *
+ * See [ParameterizedHelperTest] for more examples.
+ */
+fun generateAllEnumerations(vararg args: List<Any>): List<Array<Any>> =
+    generateAllEnumerationsIteratively(args.toList()).map { it.toTypedArray() }
+
+internal fun generateAllEnumerationsIteratively(elements: List<List<Any>>): List<List<Any>> {
+    if (elements.isEmpty()) return emptyList()
+    var number = elements.map { RadixDigit(it.size, 0) }
+    val total = elements.map { it.size }.product()
+    val result = mutableListOf<List<Any>>()
+    for (i in 0 until total) {
+        result.add(elements.mapIndexed { index, element -> element[number[index].digit] })
+        number = increment(number)
+    }
+    return result
+}
+
+internal fun increment(number: List<RadixDigit>): List<RadixDigit> {
+    var index = number.size - 1
+    var carry = 1
+    val result = mutableListOf<RadixDigit>()
+    while (index >= 0) {
+        val rd = number[index]
+        if (carry > 0) {
+            if (rd.digit < rd.radix - 1) {
+                result.add(rd.copy(digit = rd.digit + 1))
+                carry = 0
+            } else {
+                result.add(rd.copy(digit = 0))
+            }
+        } else {
+            result.add(rd)
+        }
+        index--
+    }
+    return result.reversed()
+}
+
+internal fun List<Int>.product() = this.fold(1) { acc, elem -> acc * elem }
+
+internal data class RadixDigit(val radix: Int, val digit: Int)
\ No newline at end of file
diff --git a/testutils/testutils-common/src/test/java/androidx/testutils/ParameterizedHelperTest.kt b/testutils/testutils-common/src/test/java/androidx/testutils/ParameterizedHelperTest.kt
new file mode 100644
index 0000000..43fc951e
--- /dev/null
+++ b/testutils/testutils-common/src/test/java/androidx/testutils/ParameterizedHelperTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.testutils
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ParameterizedHelperTest {
+    @Test
+    fun testIncrement() {
+        val number = listOf(RadixDigit(2, 0), RadixDigit(3, 0))
+
+        assertThat(::increment.invoke(number, 1)).isEqualTo(
+            listOf(RadixDigit(2, 0), RadixDigit(3, 1))
+        )
+        assertThat(::increment.invoke(number, 2)).isEqualTo(
+            listOf(RadixDigit(2, 0), RadixDigit(3, 2))
+        )
+        assertThat(::increment.invoke(number, 3)).isEqualTo(
+            listOf(RadixDigit(2, 1), RadixDigit(3, 0))
+        )
+        assertThat(::increment.invoke(number, 4)).isEqualTo(
+            listOf(RadixDigit(2, 1), RadixDigit(3, 1))
+        )
+        assertThat(::increment.invoke(number, 5)).isEqualTo(
+            listOf(RadixDigit(2, 1), RadixDigit(3, 2))
+        )
+        assertThat(::increment.invoke(number, 6)).isEqualTo(
+            listOf(RadixDigit(2, 0), RadixDigit(3, 0))
+        )
+        assertThat(::increment.invoke(number, 7)).isEqualTo(
+            listOf(RadixDigit(2, 0), RadixDigit(3, 1))
+        )
+    }
+
+    @Test
+    fun testProduct() {
+        assertThat(listOf<Int>().product()).isEqualTo(1)
+        assertThat(listOf(0).product()).isEqualTo(0)
+        assertThat(listOf(2).product()).isEqualTo(2)
+        assertThat(listOf(2, 3).product()).isEqualTo(6)
+    }
+
+    @Test
+    fun testEnumerations() {
+        assertThat(generateAllEnumerations()).isEmpty()
+
+        // Comparing List of Arrays doesn't work(https://github.com/google/truth/issues/928), so
+        // we're mapping it to List of Lists
+        assertThat(
+            generateAllEnumerations(listOf(false)).map { it.toList() }).isEqualTo(
+            listOf(
+                listOf<Any>(false)
+            )
+        )
+        assertThat(
+            generateAllEnumerations(listOf(false, true)).map { it.toList() }).isEqualTo(
+            listOf(
+                listOf<Any>(false),
+                listOf<Any>(true)
+            )
+        )
+        assertThat(generateAllEnumerations(listOf(false, true), listOf())).isEmpty()
+        assertThat(
+            generateAllEnumerations(
+                listOf(false, true),
+                listOf(false, true)
+            ).map { it.toList() }
+        ).isEqualTo(
+            listOf(
+                listOf(false, false),
+                listOf(false, true),
+                listOf(true, false),
+                listOf(true, true)
+            )
+        )
+        assertThat(
+            generateAllEnumerations(
+                listOf(false, true),
+                (0..2).toList(),
+                listOf("low", "hi")
+            ).map { it.toList() }
+        ).isEqualTo(
+            listOf(
+                listOf(false, 0, "low"),
+                listOf(false, 0, "hi"),
+                listOf(false, 1, "low"),
+                listOf(false, 1, "hi"),
+                listOf(false, 2, "low"),
+                listOf(false, 2, "hi"),
+                listOf(true, 0, "low"),
+                listOf(true, 0, "hi"),
+                listOf(true, 1, "low"),
+                listOf(true, 1, "hi"),
+                listOf(true, 2, "low"),
+                listOf(true, 2, "hi")
+            )
+        )
+    }
+
+    // `::f.invoke(0, 3)` is equivalent to `f(f(f(0)))`
+    private fun <T> ((T) -> T).invoke(argument: T, repeat: Int): T {
+        var result = argument
+        for (i in 0 until repeat) {
+            result = this(result)
+        }
+        return result
+    }
+
+    @Test
+    fun testInvoke() {
+        val addOne = { i: Int -> i + 1 }
+        assertThat(addOne.invoke(42, 0)).isEqualTo(42)
+        assertThat(addOne.invoke(42, 1)).isEqualTo(addOne(42))
+        assertThat(addOne.invoke(42, 2)).isEqualTo(addOne(addOne(42)))
+
+        val appendA = { str: String -> str + "a" }
+        assertThat(appendA.invoke("a", 2)).isEqualTo(appendA(appendA("a")))
+    }
+}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
index 4af7b10..eaab40e 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/AssetLoaderAjaxActivity.java
@@ -53,36 +53,30 @@
         @RequiresApi(21)
         public WebResourceResponse shouldInterceptRequest(WebView view,
                                                           WebResourceRequest request) {
-            if (mUriIdlingResource != null) {
-                mUriIdlingResource.beginLoad(request.getUrl().toString());
-            }
+            mUriIdlingResource.beginLoad(request.getUrl().toString());
             WebResourceResponse response = mAssetLoader.shouldInterceptRequest(request.getUrl());
-            if (mUriIdlingResource != null) {
-                mUriIdlingResource.endLoad(request.getUrl().toString());
-            }
+            mUriIdlingResource.endLoad(request.getUrl().toString());
             return response;
         }
 
         @Override
         @SuppressWarnings("deprecation") // use the old one for compatibility with all API levels.
         public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
-            if (mUriIdlingResource != null) {
-                mUriIdlingResource.beginLoad(url);
-            }
+            mUriIdlingResource.beginLoad(url);
             WebResourceResponse response = mAssetLoader.shouldInterceptRequest(Uri.parse(url));
-            if (mUriIdlingResource != null) {
-                mUriIdlingResource.endLoad(url);
-            }
+            mUriIdlingResource.endLoad(url);
             return response;
         }
     }
 
     private WebViewAssetLoader mAssetLoader;
     private WebView mWebView;
+
     // IdlingResource that indicates that WebView has finished loading all WebResourceRequests
     // by waiting until there are no requests made for 5000ms.
     @NonNull
-    private UriIdlingResource mUriIdlingResource;
+    private final UriIdlingResource mUriIdlingResource =
+                    new UriIdlingResource("AssetLoaderWebViewUriIdlingResource", MAX_IDLE_TIME_MS);
 
     @SuppressLint("SetJavaScriptEnabled")
     @Override
@@ -135,10 +129,6 @@
     @VisibleForTesting
     @NonNull
     public UriIdlingResource getUriIdlingResource() {
-        if (mUriIdlingResource == null) {
-            mUriIdlingResource =
-                    new UriIdlingResource("AssetLoaderWebViewUriIdlingResource", MAX_IDLE_TIME_MS);
-        }
         return mUriIdlingResource;
     }
 }