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;
}
}