diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt index f60d27449e963..9b559fb29fabc 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileConsumerPlugin.kt @@ -41,6 +41,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.LibraryExtension import com.android.build.api.variant.ApplicationVariant import com.android.build.api.variant.ApplicationVariantBuilder +import com.android.build.api.variant.KotlinMultiplatformAndroidVariant import com.android.build.api.variant.Variant import org.gradle.api.GradleException import org.gradle.api.Plugin @@ -64,12 +65,21 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : AgpPlugin( project = project, supportedAgpPlugins = - setOf(AgpPluginId.ID_ANDROID_APPLICATION_PLUGIN, AgpPluginId.ID_ANDROID_LIBRARY_PLUGIN), + setOf( + AgpPluginId.ID_ANDROID_APPLICATION_PLUGIN, + AgpPluginId.ID_ANDROID_LIBRARY_PLUGIN, + // We don't need to version check this feature. + // `com.android.kotlin.multiplatform.library` is supported starting AGP 8.10, so + // it's + // safe to assume that if this plugin were to exist, we are on a new enough version + // of AGP. + AgpPluginId.ID_ANDROID_KOTLIN_MULTIPLATFORM_LIBRARY, + ), minAgpVersionInclusive = MIN_AGP_VERSION_REQUIRED_INCLUSIVE, maxAgpVersionExclusive = MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE, ) { - // List of the non debuggable build types + // List of the non-debuggable build types private val nonDebuggableBuildTypes = mutableListOf() // The baseline profile consumer extension to access non-variant specific configuration options @@ -129,7 +139,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : override fun onApplicationFinalizeDsl(extension: ApplicationExtension) { // Here we select the build types we want to process if this is an application, - // i.e. non debuggable build types that have not been created by the app target plugin. + // i.e. non-debuggable build types that have not been created by the app target plugin. // Also exclude the build types starting with baseline profile prefix, in case the app // target plugin is also applied. @@ -170,7 +180,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : val isBaselineProfilePluginCreatedBuildType = isBaselineProfilePluginCreatedBuildType(variantBuilder.buildType) - // Note that the callback should be remove at the end, after all the variants + // Note that the callback should be removed at the end, after all the variants // have been processed. This is because the benchmark and nonMinified variants can be // disabled at any point AFTER the plugin has been applied. So checking immediately here // would tell us that the variant is enabled, while it could be disabled later. @@ -199,7 +209,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : PrintMapPropertiesForVariantTask.registerForVariant(project = project, variant = variant) // Controls whether Android Studio should see this variant. Variants created by the - // baseline profile gradle plugin are hidden by default. + // baseline profile Gradle plugin are hidden by default. if ( baselineProfileExtension.hideSyntheticBuildTypesInAndroidStudio && isBaselineProfilePluginCreatedBuildType(variant.buildType) @@ -207,8 +217,19 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : variant.experimentalProperties.put("androidx.baselineProfile.hideInStudio", true) } - // From here on, process only the non debuggable build types we previously selected. - if (variant.buildType !in nonDebuggableBuildTypes) return + // From here on, process only the non-debuggable build types we previously selected. + + // NOTE: For Kotlin Multiplatform Android Library modules, they don't seem to have a notion + // of a `buildType`. Also worth noting, is that the set of `nonDebuggableBuildTypes` are + // actually populated in the `onLibraryFinalizeDsl` block, which is not called for the + // KotlinMultiplatformAndroidComponentsExtension. This is also because they are quite + // different when compared to traditional Android Library Modules. + if ( + variant.buildType !in nonDebuggableBuildTypes && + variant !is KotlinMultiplatformAndroidVariant + ) { + return + } // This allows quick access to this variant configuration according to the override // and merge rules implemented in the PerVariantConsumerExtensionManager. @@ -222,12 +243,12 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : variantConfig = variantConfiguration, ) - // Sets the r8 rewrite baseline profile for the non debuggable variant. + // Sets the r8 rewrite baseline profile for the non-debuggable variant. variantConfiguration.baselineProfileRulesRewrite?.let { r8Utils.setRulesRewriteForVariantEnabled(variant, it) } - // Sets the r8 startup dex optimization profile for the non debuggable variant. + // Sets the r8 startup dex optimization profile for the non-debuggable variant. variantConfiguration.dexLayoutOptimization?.let { r8Utils.setDexLayoutOptimizationEnabled(variant, it) } @@ -254,7 +275,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : // When mergeIntoMain is `true` the first variant will create a task shared across // all the variants to merge, while the next variants will simply add the additional // baseline profile artifacts, modifying the existing task. - // When mergeIntoMain is `false` each variants has its own task with a single + // When mergeIntoMain is `false` each variant has its own task with a single // artifact per task, specific for that variant. // When mergeIntoMain is not specified, it's by default true for libraries and false // for apps. @@ -289,8 +310,8 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : // possible to run tests on multiple build types in the same run, when `mergeIntoMain` is // true only variants of the specific build type invoked are merged. This means that on // AGP 8.0 the `main` baseline profile is generated by only the build type `release` when - // calling `generateReleaseBaselineProfiles`. On Agp 8.1 instead, it works as intended and - // we can merge all the variants with `mergeIntoMain` true, independently from the build + // calling `generateReleaseBaselineProfiles`. On Agp 8.1 instead, it works as intended, and + // we can merge all the variants with `mergeIntoMain` true, irrespective of the build // type. data class TaskAndFolderName(val taskVariantName: String, val folderVariantName: String) val (mergeAwareTaskName, mergeAwareVariantOutput) = @@ -339,8 +360,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : // Note that the merge task is the last task only if saveInSrc is disabled. When // saveInSrc is enabled an additional task is created to copy the profile in the - // sources - // folder. + // source folder. isLastTask = !variantConfiguration.saveInSrc, ) @@ -367,7 +387,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : // This task copies the baseline profile generated from the merge task. // Note that we're reutilizing the [MergeBaselineProfileTask] because - // if the flag `mergeIntoMain` is true tasks will have the same name + // if the flag `mergeIntoMain` is true tasks will have the same name, // and we just want to add more file to copy to the same output. This is // already handled in the MergeBaselineProfileTask. val copyTaskProvider = @@ -416,7 +436,7 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : // Note that we cannot use the variant src set api // `addGeneratedSourceDirectory` since that overwrites the outputDir, // that would be re-set in the build dir. - // Also this is specific for applications: doing this for a library would + // Also, this is specific for applications: doing this for a library would // trigger a circular task dependency since the library would require // the profile in order to build the aar for the sample app and generate // the profile. @@ -606,15 +626,27 @@ private class BaselineProfileConsumerAgpPlugin(private val project: Project) : fun TaskContainer.taskMergeStartupProfile(variantName: String) = project.tasks.namedOrNull("merge", variantName, "startupProfile") - private fun createConfigurationForVariant(variant: Variant, mainConfiguration: Configuration) = - configurationManager.maybeCreate( + private fun createConfigurationForVariant( + variant: Variant, + mainConfiguration: Configuration, + ): Configuration { + val buildType = + if (variant !is KotlinMultiplatformAndroidVariant) { + "" + } else { + // Default to "release" if the variant is a KotlinMultiPlatformAndroidVariant + // This is because build types don't exist for KMP Android Library modules. + "release" + } + return configurationManager.maybeCreate( nameParts = listOf(variant.name, CONFIGURATION_NAME_BASELINE_PROFILES), canBeResolved = true, canBeConsumed = false, extendFromConfigurations = listOf(mainConfiguration), - buildType = variant.buildType ?: "", + buildType = variant.buildType ?: buildType, productFlavors = variant.productFlavors, ) + } private fun isBaselineProfilePluginCreatedBuildType(buildType: String?) = buildType?.let { diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt index 48484f2190784..e8e3f8ac51ecc 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt @@ -24,6 +24,7 @@ import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationVariant import com.android.build.api.variant.ApplicationVariantBuilder +import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.api.variant.LibraryVariant import com.android.build.api.variant.LibraryVariantBuilder @@ -48,7 +49,7 @@ internal abstract class AgpPlugin( private val maxAgpVersionExclusive: AndroidPluginVersion, ) { - // Properties that can be specified by cmd line using -P when invoking gradle. + // Properties that can be specified by cmd line using -P when invoking Gradle. val testMaxAgpVersion by lazy { project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str -> @@ -85,7 +86,7 @@ internal abstract class AgpPlugin( for (agpPluginId in supportedAgpPlugins) { project.pluginManager.withPlugin(agpPluginId.value) { foundPlugins.add(agpPluginId) - configureWithAndroidPlugin() + configureWithAndroidPlugin(agpPluginId) } } @@ -102,7 +103,7 @@ internal abstract class AgpPlugin( } } - private fun configureWithAndroidPlugin() { + private fun configureWithAndroidPlugin(agpPluginId: AgpPluginId) { fun setWarnings() { if (suppressWarnings) { @@ -172,7 +173,13 @@ internal abstract class AgpPlugin( getWarnings()?.let { warnings -> logger.setWarnings(warnings) } checkAgpVersion() } - commonComponent.beforeVariants { onBeforeVariants(it) } + // When calling onBeforeVariants for a `KotlinMultiplatformAndroidComponentsExtension` + // AGP until 9.0.0-RC3 throws an unhelpful `RuntimeException`. Once we know exactly + // which AGP version includes the patch to no longer throw the exception we can resume + // calling this method again. + if (commonComponent !is KotlinMultiplatformAndroidComponentsExtension) { + commonComponent.beforeVariants { onBeforeVariants(it) } + } commonComponent.onVariants { variantsConfigured = true onVariantBlockScheduler.onVariant(it) @@ -291,7 +298,9 @@ internal abstract class AgpPlugin( protected fun isTestModule() = testAndroidComponentExtension() != null - protected fun isLibraryModule() = libraryAndroidComponentsExtension() != null + protected fun isLibraryModule() = + libraryAndroidComponentsExtension() != null || + kotlinMultiplatformAndroidLibraryComponentsExtension() != null protected fun isApplicationModule() = applicationAndroidComponentsExtension() != null @@ -346,6 +355,10 @@ internal abstract class AgpPlugin( private fun libraryAndroidComponentsExtension(): LibraryAndroidComponentsExtension? = project.extensions.findByType(LibraryAndroidComponentsExtension::class.java) + private fun kotlinMultiplatformAndroidLibraryComponentsExtension(): + KotlinMultiplatformAndroidComponentsExtension? = + project.extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java) + private fun androidComponentsExtension(): AndroidComponentsExtension<*, *, *>? = project.extensions.findByType(AndroidComponentsExtension::class.java) } @@ -368,10 +381,11 @@ internal enum class AgpPluginId(val value: String) { ID_ANDROID_APPLICATION_PLUGIN("com.android.application"), ID_ANDROID_LIBRARY_PLUGIN("com.android.library"), ID_ANDROID_TEST_PLUGIN("com.android.test"), + ID_ANDROID_KOTLIN_MULTIPLATFORM_LIBRARY("com.android.kotlin.multiplatform.library"), } /** - * This class is basically an help to manage executing callbacks on a variant. Because of how agp + * This class is basically a helper to manage executing callbacks on a variant. Because of how agp * variants are published, there is no way to directly access it. This class stores a callback and * executes it when the variant is published in the agp onVariants callback. */ diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt index 9eecc79c4ef14..a8779d80fd2ff 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt @@ -50,6 +50,9 @@ internal const val RELEASE = "release" // Kotlin Multiplatform Plugin ID internal const val KOTLIN_MULTIPLATFORM_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" +// Kotlin Multiplatform Library Plugin ID +internal const val KOTLIN_MULTIPLATFORM_LIBRARY_PLUGIN_ID = + "com.android.kotlin.multiplatform.library" // Instrumentation runner arguments internal const val INSTRUMENTATION_ARG_ENABLED_RULES = "androidx.benchmark.enabledRules" diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/wrapper/BaselineProfileWrapperPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/wrapper/BaselineProfileWrapperPlugin.kt index bbc5806297b11..d7856be9cb09a 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/wrapper/BaselineProfileWrapperPlugin.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/wrapper/BaselineProfileWrapperPlugin.kt @@ -48,6 +48,13 @@ class BaselineProfileWrapperPlugin : Plugin { project.pluginManager.apply(BaselineProfileConsumerPlugin::class.java) } + // If this module is a kotlin multiplatform library module + project.pluginManager.withPlugin("com.android.kotlin.multiplatform.library") { + + // Applies the profile consumer plugin + project.pluginManager.apply(BaselineProfileConsumerPlugin::class.java) + } + // If this module is a test module project.pluginManager.withPlugin("com.android.test") { diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileKotlinMultiplatformLibraryTest.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileKotlinMultiplatformLibraryTest.kt new file mode 100644 index 0000000000000..1cf6c18f142da --- /dev/null +++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileKotlinMultiplatformLibraryTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.baselineprofile.gradle.consumer + +import androidx.baselineprofile.gradle.utils.BaselineProfileProjectSetupRule +import androidx.baselineprofile.gradle.utils.Fixtures +import androidx.baselineprofile.gradle.utils.build +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test + +class BaselineProfileKotlinMultiplatformLibraryTest { + @get:Rule val projectSetup = BaselineProfileProjectSetupRule() + + private val gradleRunner by lazy { projectSetup.consumer.gradleRunner } + + private fun readBaselineProfileFileContent() = + projectSetup.readBaselineProfileFileContent("androidMain") + + @Test + fun testGenerateTaskWithNoFlavorsForLibrary() { + projectSetup.consumer.setupKotlinMultiplatformLibrary() + projectSetup.producer.setupWithoutFlavors( + releaseProfileLines = + listOf( + Fixtures.CLASS_1_METHOD_1, + Fixtures.CLASS_1, + Fixtures.CLASS_2_METHOD_1, + Fixtures.CLASS_2, + ), + releaseStartupProfileLines = + listOf( + Fixtures.CLASS_3_METHOD_1, + Fixtures.CLASS_3, + Fixtures.CLASS_4_METHOD_1, + Fixtures.CLASS_4, + ), + ) + + gradleRunner.build("generateBaselineProfile") { + // Nothing to assert here. + } + + assertThat(readBaselineProfileFileContent()) + .containsExactly( + Fixtures.CLASS_1, + Fixtures.CLASS_1_METHOD_1, + Fixtures.CLASS_2, + Fixtures.CLASS_2_METHOD_1, + Fixtures.CLASS_3_METHOD_1, + Fixtures.CLASS_3, + Fixtures.CLASS_4_METHOD_1, + Fixtures.CLASS_4, + ) + } +} diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfileProjectSetupRule.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfileProjectSetupRule.kt index 6d4b7068327d1..7101793a073d9 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfileProjectSetupRule.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/BaselineProfileProjectSetupRule.kt @@ -809,4 +809,45 @@ class ConsumerModule( .trimIndent() ) } + + fun setupKotlinMultiplatformLibrary( + otherPluginsBlock: String = "", + dependenciesBlock: String = "", + dependencyOnProducerProject: Boolean = true, + additionalGradleCodeBlock: String = "", + ) { + isLibraryModule = true + // Use appendText() directly here to avoid the android() block. + rule.buildFile.appendText( + """ + plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.android.kotlin.multiplatform.library") + id("androidx.baselineprofile.consumer") + $otherPluginsBlock + } + + kotlin { + androidLibrary { + namespace = "com.example.namespace" + compileSdk = ${rule.props.compileSdk} + } + sourceSets { + androidMain.dependencies { + $dependenciesBlock + } + } + } + baselineProfile { + variants { + androidMain { + ${if (dependencyOnProducerProject) """from(project(":$producerName"))""" else ""} + } + } + } + $additionalGradleCodeBlock + """ + .trimIndent() + ) + } } diff --git a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt index 5add84c12f72f..c5480d0fb4fe7 100644 --- a/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt +++ b/benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt @@ -30,12 +30,14 @@ enum class TestAgpVersion(val versionString: String?) { companion object { fun fromVersionString(versionString: String?) = - TestAgpVersion.values().first { it.versionString == versionString } + TestAgpVersion.entries.first { it.versionString == versionString } - fun all() = values() + fun all() = TestAgpVersion.entries.toTypedArray() - fun atLeast(version: TestAgpVersion) = values().filter { it.ordinal >= version.ordinal } + fun atLeast(version: TestAgpVersion) = + TestAgpVersion.entries.filter { it.ordinal >= version.ordinal } - fun atMost(version: TestAgpVersion) = values().filter { it.ordinal <= version.ordinal } + fun atMost(version: TestAgpVersion) = + TestAgpVersion.entries.filter { it.ordinal <= version.ordinal } } } diff --git a/biometric/biometric-compose/build.gradle b/biometric/biometric-compose/build.gradle index 75292891ca407..3ab96f675ef18 100644 --- a/biometric/biometric-compose/build.gradle +++ b/biometric/biometric-compose/build.gradle @@ -51,6 +51,6 @@ androidx { } android { - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.biometric.compose" } diff --git a/biometric/biometric-compose/samples/build.gradle b/biometric/biometric-compose/samples/build.gradle index b431a4a75860e..aee47cbd8130f 100644 --- a/biometric/biometric-compose/samples/build.gradle +++ b/biometric/biometric-compose/samples/build.gradle @@ -44,6 +44,6 @@ androidx { description = "Contains the sample code for the AndroidX Biometric Compose library" } android { - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.biometric.compose.samples" } diff --git a/biometric/biometric/api/current.txt b/biometric/biometric/api/current.txt index cf302b8baf4f4..ff1bc48565c60 100644 --- a/biometric/biometric/api/current.txt +++ b/biometric/biometric/api/current.txt @@ -232,10 +232,12 @@ package androidx.biometric { ctor @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public BiometricPrompt.CryptoObject(android.security.identity.PresentationSession); ctor public BiometricPrompt.CryptoObject(java.security.Signature); ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher); + ctor @RequiresApi(android.os.Build.VERSION_CODES_FULL.BAKLAVA_1) public BiometricPrompt.CryptoObject(javax.crypto.KeyAgreement); ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac); ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long); method public javax.crypto.Cipher? getCipher(); method @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential(); + method @RequiresApi(android.os.Build.VERSION_CODES_FULL.BAKLAVA_1) public javax.crypto.KeyAgreement? getKeyAgreement(); method public javax.crypto.Mac? getMac(); method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle(); method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession(); diff --git a/biometric/biometric/api/restricted_current.txt b/biometric/biometric/api/restricted_current.txt index cf302b8baf4f4..ff1bc48565c60 100644 --- a/biometric/biometric/api/restricted_current.txt +++ b/biometric/biometric/api/restricted_current.txt @@ -232,10 +232,12 @@ package androidx.biometric { ctor @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public BiometricPrompt.CryptoObject(android.security.identity.PresentationSession); ctor public BiometricPrompt.CryptoObject(java.security.Signature); ctor public BiometricPrompt.CryptoObject(javax.crypto.Cipher); + ctor @RequiresApi(android.os.Build.VERSION_CODES_FULL.BAKLAVA_1) public BiometricPrompt.CryptoObject(javax.crypto.KeyAgreement); ctor public BiometricPrompt.CryptoObject(javax.crypto.Mac); ctor @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public BiometricPrompt.CryptoObject(long); method public javax.crypto.Cipher? getCipher(); method @Deprecated @RequiresApi(android.os.Build.VERSION_CODES.R) public android.security.identity.IdentityCredential? getIdentityCredential(); + method @RequiresApi(android.os.Build.VERSION_CODES_FULL.BAKLAVA_1) public javax.crypto.KeyAgreement? getKeyAgreement(); method public javax.crypto.Mac? getMac(); method @RequiresApi(android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) public long getOperationHandle(); method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public android.security.identity.PresentationSession? getPresentationSession(); diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle index 34c4948458ecb..8648cf190913a 100644 --- a/biometric/biometric/build.gradle +++ b/biometric/biometric/build.gradle @@ -82,7 +82,7 @@ android { } defaultConfig.consumerProguardFiles("proguard-rules.pro") namespace = "androidx.biometric" - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} } androidx { diff --git a/biometric/biometric/proguard-rules.pro b/biometric/biometric/proguard-rules.pro index ec0ef377aae3c..e9900fc954632 100644 --- a/biometric/biometric/proguard-rules.pro +++ b/biometric/biometric/proguard-rules.pro @@ -14,7 +14,7 @@ # We supply these as stubs and will only use them in older Android versions which had a framework # implementation. We don't want R8 to complain about them not being there during optimization. --dontwarn android.hardware.fingerprint.FingerprintManager.** +-dontwarn android.hardware.fingerprint.FingerprintManager** # Never inline methods, but allow shrinking and obfuscation. -keepclassmembernames,allowobfuscation,allowshrinking diff --git a/biometric/biometric/samples/build.gradle b/biometric/biometric/samples/build.gradle index fb97964810122..210fe20ac12ff 100644 --- a/biometric/biometric/samples/build.gradle +++ b/biometric/biometric/samples/build.gradle @@ -39,6 +39,6 @@ androidx { description = "Contains the sample code for the AndroidX Biometric library" } android { - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.biometric.samples" } diff --git a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java index 6032553e352a1..a6c7182d187d5 100644 --- a/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java +++ b/biometric/biometric/src/main/java/androidx/biometric/BiometricPrompt.java @@ -292,6 +292,7 @@ public static class CryptoObject { private final @Nullable Signature mSignature; private final @Nullable Cipher mCipher; private final @Nullable Mac mMac; + private final javax.crypto.@Nullable KeyAgreement mKeyAgreement; private final android.security.identity.@Nullable IdentityCredential mIdentityCredential; private final android.security.identity.@Nullable PresentationSession mPresentationSession; private final long mOperationHandle; @@ -305,6 +306,7 @@ public CryptoObject(@NonNull Signature signature) { mSignature = signature; mCipher = null; mMac = null; + mKeyAgreement = null; mIdentityCredential = null; mPresentationSession = null; mOperationHandle = 0; @@ -319,6 +321,7 @@ public CryptoObject(@NonNull Cipher cipher) { mSignature = null; mCipher = cipher; mMac = null; + mKeyAgreement = null; mIdentityCredential = null; mPresentationSession = null; mOperationHandle = 0; @@ -333,6 +336,7 @@ public CryptoObject(@NonNull Mac mac) { mSignature = null; mCipher = null; mMac = mac; + mKeyAgreement = null; mIdentityCredential = null; mPresentationSession = null; mOperationHandle = 0; @@ -352,6 +356,7 @@ public CryptoObject( mSignature = null; mCipher = null; mMac = null; + mKeyAgreement = null; mIdentityCredential = identityCredential; mPresentationSession = null; mOperationHandle = 0; @@ -369,11 +374,28 @@ public CryptoObject( mSignature = null; mCipher = null; mMac = null; + mKeyAgreement = null; mIdentityCredential = null; mPresentationSession = presentationSession; mOperationHandle = 0; } + /** + * Creates a crypto object that wraps the given key agreement object. + * + * @param keyAgreement The key agreement to be associated with this crypto object. + */ + @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1) + public CryptoObject(javax.crypto.@NonNull KeyAgreement keyAgreement) { + mSignature = null; + mCipher = null; + mMac = null; + mKeyAgreement = keyAgreement; + mIdentityCredential = null; + mPresentationSession = null; + mOperationHandle = 0; + } + /** * Create from an operation handle. * @see CryptoObject#getOperationHandle() @@ -385,12 +407,12 @@ public CryptoObject(long operationHandle) { mSignature = null; mCipher = null; mMac = null; + mKeyAgreement = null; mIdentityCredential = null; mPresentationSession = null; mOperationHandle = operationHandle; } - /** * Gets the signature object associated with this crypto object. * @@ -440,6 +462,16 @@ public CryptoObject(long operationHandle) { return mPresentationSession; } + /** + * Gets the key agreement object associated with this crypto object. + * + * @return The key agreement, or {@code null} if none is associated with this object. + */ + @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1) + public javax.crypto.@Nullable KeyAgreement getKeyAgreement() { + return mKeyAgreement; + } + /** * Returns the {@code operationHandle} associated with this object or 0 if none. * The {@code operationHandle} is the underlying identifier associated with diff --git a/biometric/biometric/src/main/java/androidx/biometric/utils/CryptoObjectUtils.java b/biometric/biometric/src/main/java/androidx/biometric/utils/CryptoObjectUtils.java index 1bb9141ae8504..68d0dace68d52 100644 --- a/biometric/biometric/src/main/java/androidx/biometric/utils/CryptoObjectUtils.java +++ b/biometric/biometric/src/main/java/androidx/biometric/utils/CryptoObjectUtils.java @@ -117,6 +117,15 @@ private CryptoObjectUtils() { } } + // Key agreement is only supported on API 36.1 and above. + if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1) { + final javax.crypto.KeyAgreement keyAgreement = + Api36MinorImpl.getKeyAgreement(cryptoObject); + if (keyAgreement != null) { + return new BiometricPrompt.CryptoObject(keyAgreement); + } + } + // Operation handle is only supported on API 35 and above. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { // This should be the bottom one and only be reachable when cryptoObject was @@ -180,6 +189,14 @@ private CryptoObjectUtils() { } } + // Key agreement is only supported on API 36.1 and above. + if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1) { + final javax.crypto.KeyAgreement keyAgreement = cryptoObject.getKeyAgreement(); + if (keyAgreement != null) { + return Api36MinorImpl.create(keyAgreement); + } + } + // Operation handle is only supported on API 35 and above. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { final long operationHandle = cryptoObject.getOperationHandleCryptoObject(); @@ -215,7 +232,7 @@ public static long getOperationHandle(BiometricPrompt.@Nullable CryptoObject cry * {@link androidx.biometric.internal.FingerprintManagerCompat}. * * @param cryptoObject A crypto object from - * {@link androidx.biometric.internal.FingerprintManagerCompat}. + * {@link androidx.biometric.internal.FingerprintManagerCompat}. * @return An equivalent {@link androidx.biometric.BiometricPrompt.CryptoObject} instance. */ @SuppressWarnings("deprecation") @@ -291,6 +308,12 @@ public static long getOperationHandle(BiometricPrompt.@Nullable CryptoObject cry return null; } + if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1 + && cryptoObject.getKeyAgreement() != null) { + Log.e(TAG, "Key agreement is not supported by FingerprintManager."); + return null; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { Log.e(TAG, "Operation handle is not supported by FingerprintManager."); return null; @@ -341,6 +364,39 @@ public static long getOperationHandle(BiometricPrompt.@Nullable CryptoObject cry } } + + @RequiresApi(Build.VERSION_CODES_FULL.BAKLAVA_1) + private static class Api36MinorImpl { + // Prevent instantiation. + private Api36MinorImpl() { + } + + /** + * Creates an instance of the framework class + * { @link android.hardware.biometrics.BiometricPrompt.CryptoObject} from the given + * key agreement. + * + * @param keyAgreement The key agreement to be wrapped. + * @return An instance of { @link android.hardware.biometrics.BiometricPrompt.CryptoObject}. + */ + static android.hardware.biometrics.BiometricPrompt.@NonNull CryptoObject create( + javax.crypto.@NonNull KeyAgreement keyAgreement) { + return new android.hardware.biometrics.BiometricPrompt.CryptoObject(keyAgreement); + } + + /** + * Gets the key agreement associated with the given crypto object, if any. + * + * @param crypto An instance of + * { @link android.hardware.biometrics.BiometricPrompt.CryptoObject}. + * @return The wrapped key agreement object, or { @code null}. + */ + static javax.crypto.@Nullable KeyAgreement getKeyAgreement( + android.hardware.biometrics.BiometricPrompt.@NonNull CryptoObject crypto) { + return crypto.getKeyAgreement(); + } + } + /** * Nested class to avoid verification errors for methods introduced in Android 15.0 (API 35). */ diff --git a/biometric/biometric/src/test/java/androidx/biometric/utils/CryptoObjectUtilsTest.java b/biometric/biometric/src/test/java/androidx/biometric/utils/CryptoObjectUtilsTest.java index 6a2d68918654b..6c5dc37999ab2 100644 --- a/biometric/biometric/src/test/java/androidx/biometric/utils/CryptoObjectUtilsTest.java +++ b/biometric/biometric/src/test/java/androidx/biometric/utils/CryptoObjectUtilsTest.java @@ -51,6 +51,8 @@ public class CryptoObjectUtilsTest { private Mac mMac; @Mock private Signature mSignature; + @Mock + private javax.crypto.KeyAgreement mKeyAgreement; @Test @Config(minSdk = Build.VERSION_CODES.P) @@ -141,6 +143,22 @@ public void testUnwrapFromBiometricPrompt_WithPresentationSessionCryptoObject() assertThat(unwrappedCrypto.getPresentationSession()).isEqualTo(presentationSession); } + @Test + @Config(minSdk = Build.VERSION_CODES_FULL.BAKLAVA_1) + public void testUnwrapFromBiometricPrompt_WithKeyAgreementCryptoObject() { + final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCrypto = + new android.hardware.biometrics.BiometricPrompt.CryptoObject(mKeyAgreement); + + final BiometricPrompt.CryptoObject unwrappedCrypto = + CryptoObjectUtils.unwrapFromBiometricPrompt(wrappedCrypto); + + assertThat(unwrappedCrypto).isNotNull(); + assertThat(unwrappedCrypto.getCipher()).isNull(); + assertThat(unwrappedCrypto.getSignature()).isNull(); + assertThat(unwrappedCrypto.getMac()).isNull(); + assertThat(unwrappedCrypto.getKeyAgreement()).isEqualTo(mKeyAgreement); + } + @Test @Config(minSdk = Build.VERSION_CODES.P) public void testWrapForBiometricPrompt_WithNullCryptoObject() { @@ -229,6 +247,22 @@ public void testWrapForBiometricPrompt_WithPresentationSessionCryptoObject() { assertThat(wrappedCrypto.getPresentationSession()).isEqualTo(presentationSession); } + @Test + @Config(minSdk = Build.VERSION_CODES_FULL.BAKLAVA_1) + public void testWrapForBiometricPrompt_WithKeyAgreementCryptoObject() { + final BiometricPrompt.CryptoObject unwrappedCrypto = + new BiometricPrompt.CryptoObject(mKeyAgreement); + + final android.hardware.biometrics.BiometricPrompt.CryptoObject wrappedCrypto = + CryptoObjectUtils.wrapForBiometricPrompt(unwrappedCrypto); + + assertThat(wrappedCrypto).isNotNull(); + assertThat(wrappedCrypto.getCipher()).isNull(); + assertThat(wrappedCrypto.getSignature()).isNull(); + assertThat(wrappedCrypto.getMac()).isNull(); + assertThat(wrappedCrypto.getKeyAgreement()).isEqualTo(mKeyAgreement); + } + @Test public void testUnwrapFromFingerprintManager_WithNullCryptoObject() { assertThat(CryptoObjectUtils.unwrapFromFingerprintManager(null)).isNull(); diff --git a/biometric/integration-tests/testapp-compose/build.gradle b/biometric/integration-tests/testapp-compose/build.gradle index c9b4e962f69cc..04c034eb74bc3 100644 --- a/biometric/integration-tests/testapp-compose/build.gradle +++ b/biometric/integration-tests/testapp-compose/build.gradle @@ -49,7 +49,7 @@ dependencies { } android { - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} namespace = "androidx.biometric.integration.testappcompose" defaultConfig { applicationId = "androidx.biometric.integration.testappcompose" diff --git a/biometric/integration-tests/testapp/build.gradle b/biometric/integration-tests/testapp/build.gradle index 219c9936d4689..d7c247c2c3703 100644 --- a/biometric/integration-tests/testapp/build.gradle +++ b/biometric/integration-tests/testapp/build.gradle @@ -20,7 +20,7 @@ plugins { } android { - compileSdk { version = release(35) } + compileSdk { version = release(36) { minorApiLevel = 1 }} defaultConfig { applicationId = "androidx.biometric.integration.testapp" } diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StrictMode.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StrictMode.kt new file mode 100644 index 0000000000000..a1cb3ac31c0bc --- /dev/null +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StrictMode.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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.camera2.pipe + +import androidx.annotation.RestrictTo +import androidx.camera.camera2.pipe.core.Log + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +internal class StrictMode(val enabled: Boolean) { + inline fun check(value: Boolean, crossinline message: () -> String) { + if (!value) { + val failureMessage = message() + if (!enabled) { + Log.warn { failureMessage } + return + } + throw IllegalStateException(failureMessage) + } + } +} diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt index 8e4e56954a453..f7f3a26229cf1 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt @@ -29,6 +29,7 @@ import androidx.camera.camera2.pipe.CameraId import androidx.camera.camera2.pipe.CameraSurfaceManager import androidx.camera.camera2.pipe.StreamGraph import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.SurfaceTracker import androidx.camera.camera2.pipe.config.Camera2ControllerScope import androidx.camera.camera2.pipe.core.DurationNs @@ -63,6 +64,7 @@ internal class Camera2CameraController constructor( private val scope: CoroutineScope, private val threads: Threads, + private val strictMode: StrictMode, private val graphConfig: CameraGraph.Config, private val graphListener: GraphListener, private val surfaceTracker: SurfaceTracker, @@ -232,6 +234,7 @@ constructor( graphConfig.flags, concurrentSessionSequencer, streamGraph, + strictMode, threads, scope, ) diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt index f41f5bef11823..0167b4510b931 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequence.kt @@ -35,6 +35,7 @@ import androidx.camera.camera2.pipe.RequestMetadata import androidx.camera.camera2.pipe.SensorTimestamp import androidx.camera.camera2.pipe.StreamGraph import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.Debug import kotlinx.coroutines.CompletableDeferred @@ -53,6 +54,7 @@ internal class Camera2CaptureSequence( private val surfaceToStreamMap: Map, private val surfaceToOutputMap: Map, private val streamGraph: StreamGraph, + private val strictMode: StrictMode, ) : Camera2CaptureCallback, CameraCaptureSession.CaptureCallback(), @@ -287,7 +289,7 @@ internal class Camera2CaptureSequence( hasStarted.complete(Unit) sequenceListener.onCaptureSequenceComplete(this) - check(sequenceNumber == captureSequenceId) { + strictMode.check(sequenceNumber == captureSequenceId) { "onCaptureSequenceCompleted was invoked on $sequenceNumber, but expected " + "$captureSequenceId!" } @@ -309,7 +311,7 @@ internal class Camera2CaptureSequence( hasStarted.complete(Unit) sequenceListener.onCaptureSequenceComplete(this) - check(sequenceNumber == captureSequenceId) { + strictMode.check(sequenceNumber == captureSequenceId) { "onCaptureSequenceAborted was invoked on $sequenceNumber, but expected " + "$captureSequenceId!" } diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt index c1d2b8ce326f8..63f7f0a7fc510 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt @@ -36,6 +36,7 @@ import androidx.camera.camera2.pipe.RequestNumber import androidx.camera.camera2.pipe.RequestTemplate import androidx.camera.camera2.pipe.StreamGraph import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.Debug import androidx.camera.camera2.pipe.core.Log import androidx.camera.camera2.pipe.core.Log.MonitoredLogMessages.REPEATING_REQUEST_STARTED_TIMEOUT @@ -63,6 +64,7 @@ constructor( private val graphConfig: CameraGraph.Config, private val streamGraph: StreamGraphImpl, private val quirks: Camera2Quirks, + private val strictMode: StrictMode, ) : Camera2CaptureSequenceProcessorFactory { @Suppress("UNCHECKED_CAST") override fun create( @@ -77,6 +79,7 @@ constructor( streamToSurfaceMap, outputToSurfaceMap, streamGraph, + strictMode, quirks.shouldWaitForRepeatingRequestStartOnDisconnect(graphConfig), ) as CaptureSequenceProcessor> @@ -101,6 +104,7 @@ internal class Camera2CaptureSequenceProcessor( private val streamToSurfaceMap: Map, private val outputToSurfaceMap: Map, private val streamGraph: StreamGraph, + private val strictMode: StrictMode, private val awaitRepeatingRequestOnDisconnect: Boolean = false, ) : CaptureSequenceProcessor { private val debugId = captureSequenceProcessorDebugIds.incrementAndGet() @@ -302,6 +306,7 @@ internal class Camera2CaptureSequenceProcessor( surfaceToStreamMap, surfaceToOutputMap, streamGraph, + strictMode, ) } diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt index 7c3658ae2d0b4..5eafa6028ef30 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt @@ -22,7 +22,7 @@ import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBefo import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.EXACT import androidx.camera.camera2.pipe.CameraId import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy -import androidx.camera.camera2.pipe.CameraPipe +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.compat.Camera2Quirks.Companion.SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP import javax.inject.Inject import javax.inject.Singleton @@ -33,7 +33,7 @@ internal class Camera2Quirks @Inject constructor( private val metadataProvider: Camera2MetadataProvider, - private val cameraPipeFlags: CameraPipe.Flags, + private val strictMode: StrictMode, ) { /** * A quirk that waits for the last repeating capture request to start before stopping the @@ -47,16 +47,17 @@ constructor( internal fun shouldWaitForRepeatingRequestStartOnDisconnect( graphConfig: CameraGraph.Config ): Boolean { - val isStrictModeOn = cameraPipeFlags.strictModeEnabled + if (strictMode.enabled) { + return false + } // First, check for overrides. graphConfig.flags.awaitRepeatingRequestOnDisconnect?.let { - return !isStrictModeOn && it + return it } // Then we verify whether we need this quirk based on hardware level. - return !isStrictModeOn && - metadataProvider.awaitCameraMetadata(graphConfig.camera).isHardwareLevelLegacy + return metadataProvider.awaitCameraMetadata(graphConfig.camera).isHardwareLevelLegacy } /** @@ -71,8 +72,11 @@ constructor( * - API levels: 24 (N) – 28 (P) */ internal fun shouldCreateEmptyCaptureSessionBeforeClosing(cameraId: CameraId): Boolean { + if (strictMode.enabled) { + return false + } + return Build.VERSION.SDK_INT in (Build.VERSION_CODES.N..Build.VERSION_CODES.P) && - !cameraPipeFlags.strictModeEnabled && metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy } @@ -85,9 +89,12 @@ constructor( * - Device(s): Camera devices on hardware level LEGACY * - API levels: All */ - internal fun shouldWaitForCameraDeviceOnClosed(cameraId: CameraId): Boolean = - !cameraPipeFlags.strictModeEnabled && - metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy + internal fun shouldWaitForCameraDeviceOnClosed(cameraId: CameraId): Boolean { + if (strictMode.enabled) { + return false + } + return metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy + } /** * A quirk that closes the camera devices before creating a new capture session. This is needed @@ -104,7 +111,9 @@ constructor( * - API levels: 23 (M) – 31 (S_V2) */ internal fun shouldCloseCameraBeforeCreatingCaptureSession(cameraId: CameraId): Boolean { - val isStrictModeEnabled = cameraPipeFlags.strictModeEnabled + if (strictMode.enabled) { + return false + } val isLegacyDevice = Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 && metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy @@ -112,7 +121,7 @@ constructor( "motorola".equals(Build.BRAND, ignoreCase = true) && "moto e20".equals(Build.MODEL, ignoreCase = true) && cameraId.value == "1" - return !isStrictModeEnabled && (isLegacyDevice || isQuirkyDevice) + return isLegacyDevice || isQuirkyDevice } /** @@ -127,7 +136,7 @@ constructor( * - API levels: Before 34 (U) */ internal fun getRepeatingRequestFrameCountForCapture(graphConfigFlags: CameraGraph.Flags): Int { - if (cameraPipeFlags.strictModeEnabled) { + if (strictMode.enabled) { return 0 } diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt index 15e14dfc8dd4d..a5ebe96e5c38d 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionState.kt @@ -29,6 +29,7 @@ import androidx.camera.camera2.pipe.GraphState import androidx.camera.camera2.pipe.OutputId import androidx.camera.camera2.pipe.StreamGraph import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.Debug import androidx.camera.camera2.pipe.core.Log import androidx.camera.camera2.pipe.core.Threads @@ -73,6 +74,7 @@ internal class CaptureSessionState( private val cameraGraphFlags: CameraGraph.Flags, private val concurrentSessionSequencer: ConcurrentSessionSequencer?, private val streamGraph: StreamGraph, + private val strictMode: StrictMode, private val threads: Threads, private val scope: CoroutineScope, ) : CameraCaptureSessionWrapper.StateCallback { diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt index e1d83b434b69c..7ff8652eb29c9 100644 --- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt +++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt @@ -29,6 +29,7 @@ import androidx.camera.camera2.pipe.CameraDevices import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.CameraPipe.CameraMetadataConfig import androidx.camera.camera2.pipe.CameraSurfaceManager +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.compat.AndroidDevicePolicyManagerWrapper import androidx.camera.camera2.pipe.compat.AudioRestrictionController import androidx.camera.camera2.pipe.compat.ConcurrentSessionSequencers @@ -200,6 +201,10 @@ internal abstract class CameraPipeModule { @Singleton @Provides fun provideCameraSurfaceManager() = CameraSurfaceManager() + @Singleton + @Provides + fun provideStrictMode(flags: CameraPipe.Flags) = StrictMode(flags.strictModeEnabled) + @Singleton @Provides fun provideCameraDeviceSetupCompatFactory( diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CameraControllerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CameraControllerTest.kt index 641bfee7e5b1b..dd65796400157 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CameraControllerTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CameraControllerTest.kt @@ -26,11 +26,11 @@ import androidx.camera.camera2.pipe.CameraError import androidx.camera.camera2.pipe.CameraGraph import androidx.camera.camera2.pipe.CameraGraphId import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.CameraStream import androidx.camera.camera2.pipe.CameraSurfaceManager import androidx.camera.camera2.pipe.StreamFormat import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.SurfaceTracker import androidx.camera.camera2.pipe.core.TimeSource import androidx.camera.camera2.pipe.core.TimestampNs @@ -98,7 +98,7 @@ class Camera2CameraControllerTest { private val fakeCamera2Quirks = Camera2Quirks( FakeCamera2MetadataProvider(mapOf(cameraId to fakeCameraMetadata)), - cameraPipeFlags = CameraPipe.Flags(), + StrictMode(false), ) private val fakeTimeSource: TimeSource = mock() private val fakeGraphId = CameraGraphId.nextId() @@ -124,6 +124,7 @@ class Camera2CameraControllerTest { Camera2CameraController( testBackgroundScope, fakeThreads, + StrictMode(true), fakeGraphConfig, fakeGraphListener, fakeSurfaceTracker, diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt index 106617804d276..057be56b98b13 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessorTest.kt @@ -31,6 +31,7 @@ import androidx.camera.camera2.pipe.OutputStream import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.RequestTemplate import androidx.camera.camera2.pipe.StreamFormat +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.graph.StreamGraphImpl import androidx.camera.camera2.pipe.testing.FakeCameraDeviceWrapper import androidx.camera.camera2.pipe.testing.FakeCaptureSequenceListener @@ -196,6 +197,7 @@ internal class Camera2CaptureSequenceProcessorTest { stream2.outputs.single().id to surface2, ), streamGraph, + StrictMode(true), ) val sequence = @@ -239,6 +241,7 @@ internal class Camera2CaptureSequenceProcessorTest { mapOf(stream1.id to surface1), mapOf(stream1.outputs.single().id to surface1), streamGraph, + StrictMode(true), ) val captureSequence = captureSequenceProcessor.build( @@ -267,6 +270,7 @@ internal class Camera2CaptureSequenceProcessorTest { mapOf(stream1.id to surface1), mapOf(stream1.outputs.single().id to surface1), streamGraph, + StrictMode(true), ) // Key part is that only stream1 has a surface, but stream2 is requested. @@ -294,6 +298,7 @@ internal class Camera2CaptureSequenceProcessorTest { mapOf(stream1.id to surface1), mapOf(stream1.outputs.single().id to surface1), streamGraph, + StrictMode(true), ) val captureSequence = captureSequenceProcessor.build( @@ -328,6 +333,7 @@ internal class Camera2CaptureSequenceProcessorTest { stream4.outputs.single().id to surface4, ), highSpeedStreamGraph, + StrictMode(true), ) val sequence = diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceTest.kt index 6ac263b5ad323..288018824194e 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceTest.kt @@ -38,6 +38,7 @@ import androidx.camera.camera2.pipe.RequestNumber import androidx.camera.camera2.pipe.SensorTimestamp import androidx.camera.camera2.pipe.StreamFormat import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.graph.StreamGraphImpl import androidx.camera.camera2.pipe.testing.FakeCameraMetadata import androidx.camera.camera2.pipe.testing.FakeRequestMetadata @@ -84,6 +85,7 @@ internal class Camera2CaptureSequenceTest { mapOf(surface to streamId), mapOf(surface to outputId), streamGraph, + StrictMode(true), ) @Before @@ -183,6 +185,7 @@ internal class Camera2CaptureSequenceTest { mapOf(surface1 to stream.id), mapOf(surface1 to output1.id, surface2 to output2.id), streamGraph, + StrictMode(true), ) val frameNumber1: Long = 5 diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt index 1141bc93de980..4ce57cb7752b4 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2DeviceManagerTest.kt @@ -21,7 +21,7 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED import android.hardware.camera2.CameraDevice import android.os.Build import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.Permissions import androidx.camera.camera2.pipe.core.TimeSource import androidx.camera.camera2.pipe.core.TimestampNs @@ -85,7 +85,7 @@ internal class PruningCamera2DeviceManagerImplTest { val fakeCamera2MetadataProvider = FakeCamera2MetadataProvider(mapOf(cameraId to fakeCameraMetadata)) val fakeCamera2Quirks = - Camera2Quirks(fakeCamera2MetadataProvider, CameraPipe.Flags()) + Camera2Quirks(fakeCamera2MetadataProvider, StrictMode(false)) val fakeAndroidCameraState = AndroidCameraState( cameraId, @@ -919,7 +919,7 @@ internal class PruningCamera2DeviceManagerImplTest { val fakeCameraMetadata = FakeCameraMetadata(cameraId = cameraId) val fakeCamera2MetadataProvider = FakeCamera2MetadataProvider(mapOf(cameraId to fakeCameraMetadata)) - val fakeCamera2Quirks = Camera2Quirks(fakeCamera2MetadataProvider, CameraPipe.Flags()) + val fakeCamera2Quirks = Camera2Quirks(fakeCamera2MetadataProvider, StrictMode(false)) val fakeAndroidCameraState = AndroidCameraState( cameraId, diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2QuirksTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2QuirksTest.kt index ae5d435d3b516..2258517455bc6 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2QuirksTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/Camera2QuirksTest.kt @@ -21,7 +21,7 @@ import android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEG import android.os.Build import androidx.camera.camera2.pipe.CameraGraph import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.testing.FakeCamera2MetadataProvider import androidx.camera.camera2.pipe.testing.FakeCameraMetadata import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner @@ -58,7 +58,7 @@ class Camera2QuirksTest { fun shouldWaitForRepeatingRequestStartOnDisconnect_strict_mode_off() { // strict mode off by default val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) // verify function return true assertThat(camera2Quirks.shouldWaitForRepeatingRequestStartOnDisconnect(graphConfig)) @@ -69,10 +69,7 @@ class Camera2QuirksTest { fun shouldWaitForRepeatingRequestStartOnDisconnect_strict_mode_on() { // strict mode on val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldWaitForRepeatingRequestStartOnDisconnect(graphConfig)) @@ -84,7 +81,7 @@ class Camera2QuirksTest { fun shouldCreateEmptyCaptureSessionBeforeClosing_strict_mode_off_within_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) // verify assertThat(camera2Quirks.shouldCreateEmptyCaptureSessionBeforeClosing(fakeCameraId)) @@ -96,10 +93,7 @@ class Camera2QuirksTest { fun shouldCreateEmptyCaptureSessionBeforeClosing_strict_mode_off_outside_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldCreateEmptyCaptureSessionBeforeClosing(fakeCameraId)) @@ -111,10 +105,7 @@ class Camera2QuirksTest { fun shouldCreateEmptyCaptureSessionBeforeClosing_strict_mode_on_within_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldCreateEmptyCaptureSessionBeforeClosing(fakeCameraId)) @@ -126,10 +117,7 @@ class Camera2QuirksTest { fun shouldCreateEmptyCaptureSessionBeforeClosing_strict_mode_on_outside_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldCreateEmptyCaptureSessionBeforeClosing(fakeCameraId)) @@ -140,7 +128,7 @@ class Camera2QuirksTest { fun shouldWaitForCameraDeviceOnClosed_strict_mode_off() { // strict mode off by default val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) assertThat(camera2Quirks.shouldWaitForCameraDeviceOnClosed(fakeCameraId)).isTrue() } @@ -149,10 +137,7 @@ class Camera2QuirksTest { fun shouldWaitForCameraDeviceOnClosed_strict_mode_on() { // strict mode on val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) assertThat(camera2Quirks.shouldWaitForCameraDeviceOnClosed(fakeCameraId)).isFalse() } @@ -162,7 +147,7 @@ class Camera2QuirksTest { fun shouldCloseCameraBeforeCreatingCaptureSession_strict_mode_off_within_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) // verify assertThat(camera2Quirks.shouldCloseCameraBeforeCreatingCaptureSession(fakeCameraId)) @@ -174,7 +159,7 @@ class Camera2QuirksTest { fun shouldCloseCameraBeforeCreatingCaptureSession_strict_mode_off_outside_sdk_range() { // strict mode off by default val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) // verify assertThat(camera2Quirks.shouldCloseCameraBeforeCreatingCaptureSession(fakeCameraId)) @@ -185,10 +170,7 @@ class Camera2QuirksTest { @Test fun shouldCloseCameraBeforeCreatingCaptureSession_strict_mode_on_within_sdk_range() { val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldCloseCameraBeforeCreatingCaptureSession(fakeCameraId)) @@ -199,10 +181,7 @@ class Camera2QuirksTest { @Test fun shouldCloseCameraBeforeCreatingCaptureSession_strict_mode_on_outside_sdk_range() { val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(strictModeEnabled = true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) // verify assertThat(camera2Quirks.shouldCloseCameraBeforeCreatingCaptureSession(fakeCameraId)) @@ -213,7 +192,7 @@ class Camera2QuirksTest { @Test fun getRepeatingRequestFrameCountForCapture_strict_mode_off() { val camera2Quirks = - Camera2Quirks(metadataProvider = metadataProvider, cameraPipeFlags = CameraPipe.Flags()) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(false)) val repeat = 3u val flags = @@ -231,10 +210,7 @@ class Camera2QuirksTest { @Test fun getRepeatingRequestFrameCountForCapture_strict_mode_on() { val camera2Quirks = - Camera2Quirks( - metadataProvider = metadataProvider, - cameraPipeFlags = CameraPipe.Flags(true), - ) + Camera2Quirks(metadataProvider = metadataProvider, strictMode = StrictMode(true)) val repeat = 3u val flags = diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt index cd43d047cd1f8..a4139391a6fa6 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactoryTest.kt @@ -37,6 +37,7 @@ import androidx.camera.camera2.pipe.OutputId import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.StreamFormat import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.config.Camera2ControllerScope import androidx.camera.camera2.pipe.config.CameraGraphScope import androidx.camera.camera2.pipe.config.CameraPipeModule @@ -144,6 +145,7 @@ internal class CaptureSessionFactoryTest { ), concurrentSessionSequencer = null, streamMap, + StrictMode(true), threads, this, ), diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt index 76f3a666d9690..d65376f757163 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/CaptureSessionStateTest.kt @@ -31,6 +31,7 @@ import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.StreamFormat import androidx.camera.camera2.pipe.StreamGraph import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.SystemTimeSource import androidx.camera.camera2.pipe.graph.GraphListener import androidx.camera.camera2.pipe.graph.StreamGraphImpl @@ -134,6 +135,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -161,6 +163,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -193,6 +196,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -231,6 +235,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -259,6 +264,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -288,6 +294,7 @@ class CaptureSessionStateTest { cameraGraphFlags, concurrentSessionSequencer = null, streamGraph, + StrictMode(true), fakeThreads, this, ) @@ -316,6 +323,7 @@ class CaptureSessionStateTest { CameraGraph.Flags(closeCaptureSessionOnDisconnect = true), concurrentSessionSequencer = null, streamGraph, + StrictMode(false), fakeThreads, this, ) diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt index 81b5b0f930478..2153524db7ae9 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpenerTest.kt @@ -33,7 +33,7 @@ import androidx.camera.camera2.pipe.CameraError.Companion.ERROR_UNKNOWN_EXCEPTIO import androidx.camera.camera2.pipe.CameraExtensionMetadata import androidx.camera.camera2.pipe.CameraId import androidx.camera.camera2.pipe.CameraMetadata -import androidx.camera.camera2.pipe.CameraPipe +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.DurationNs import androidx.camera.camera2.pipe.core.Timestamps import androidx.camera.camera2.pipe.internal.CameraErrorListener @@ -123,7 +123,7 @@ class RetryingCameraStateOpenerTest { } } - private val fakeCamera2Quirks = Camera2Quirks(camera2MetadataProvider, CameraPipe.Flags()) + private val fakeCamera2Quirks = Camera2Quirks(camera2MetadataProvider, StrictMode(false)) private val fakeTimeSource = FakeTimeSource() private val cameraDeviceCloser = FakeCamera2DeviceCloser() diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt index a1126859cd17a..605ebc7a54191 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt @@ -22,8 +22,8 @@ import android.os.Looper.getMainLooper import android.view.Surface import androidx.camera.camera2.pipe.CameraError import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.RequestTemplate +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.core.SystemTimeSource import androidx.camera.camera2.pipe.core.TimeSource import androidx.camera.camera2.pipe.core.Timestamps @@ -296,7 +296,7 @@ internal class AndroidCameraDeviceTest { private val fakeCamera2Quirks = Camera2Quirks( FakeCamera2MetadataProvider(mapOf(cameraId to fakeCameraMetadata)), - CameraPipe.Flags(), + StrictMode(false), ) private val now = Timestamps.now(timeSource) private val cameraErrorListener = diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt index b217df3f24d20..14806fbddcf93 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt @@ -23,13 +23,13 @@ import android.view.Surface import androidx.camera.camera2.pipe.CameraError import androidx.camera.camera2.pipe.CameraGraphId import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.GraphState import androidx.camera.camera2.pipe.GraphState.GraphStateError import androidx.camera.camera2.pipe.GraphState.GraphStateStopped import androidx.camera.camera2.pipe.GraphStateListener import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.compat.Camera2Quirks import androidx.camera.camera2.pipe.testing.FakeCamera2MetadataProvider import androidx.camera.camera2.pipe.testing.FakeCameraMetadata @@ -95,7 +95,7 @@ internal class GraphProcessorTest { FakeCamera2MetadataProvider( mapOf(CameraId("0") to FakeCameraMetadata(cameraId = CameraId("0"))) ), - cameraPipeFlags = CameraPipe.Flags(), + strictMode = StrictMode(false), ), ) diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphListenersImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphListenersImplTest.kt index 2c063855f47b2..5687d9c8af4c7 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphListenersImplTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphListenersImplTest.kt @@ -20,9 +20,9 @@ import android.graphics.SurfaceTexture import android.view.Surface import androidx.camera.camera2.pipe.CameraGraphId import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.compat.Camera2Quirks import androidx.camera.camera2.pipe.graph.GraphProcessorImpl import androidx.camera.camera2.pipe.graph.GraphRequestProcessor @@ -70,7 +70,7 @@ class CameraGraphListenersImplTest { FakeCamera2MetadataProvider( mapOf(CameraId("0") to FakeCameraMetadata(cameraId = CameraId("0"))) ), - cameraPipeFlags = CameraPipe.Flags(), + strictMode = StrictMode(false), ), ) diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphParametersImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphParametersImplTest.kt index d98a465c3a40f..e331ccc984dd5 100644 --- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphParametersImplTest.kt +++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/internal/CameraGraphParametersImplTest.kt @@ -20,9 +20,9 @@ import android.hardware.camera2.CaptureRequest import android.view.Surface import androidx.camera.camera2.pipe.CameraGraphId import androidx.camera.camera2.pipe.CameraId -import androidx.camera.camera2.pipe.CameraPipe import androidx.camera.camera2.pipe.Request import androidx.camera.camera2.pipe.StreamId +import androidx.camera.camera2.pipe.StrictMode import androidx.camera.camera2.pipe.compat.Camera2Quirks import androidx.camera.camera2.pipe.graph.GraphProcessorImpl import androidx.camera.camera2.pipe.graph.GraphRequestProcessor @@ -72,7 +72,7 @@ class CameraGraphParametersImplTest { FakeCamera2MetadataProvider( mapOf(CameraId("0") to FakeCameraMetadata(cameraId = CameraId("0"))) ), - cameraPipeFlags = CameraPipe.Flags(), + strictMode = StrictMode(false), ), ) private val surfaceMap = mapOf(StreamId(0) to Surface(SurfaceTexture(1))) diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle index 89258a6a6af00..00841035cc24e 100644 --- a/lifecycle/lifecycle-viewmodel/build.gradle +++ b/lifecycle/lifecycle-viewmodel/build.gradle @@ -88,9 +88,9 @@ androidXMultiplatform { implementation(libs.testRunner) } - unixMain.dependsOn(nativeMain) - appleMain.dependsOn(unixMain) - linuxMain.dependsOn(unixMain) + nativeMain.dependencies { + implementation(libs.atomicFu) + } webTest.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") diff --git a/lifecycle/lifecycle-viewmodel/src/appleMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.darwin.kt b/lifecycle/lifecycle-viewmodel/src/appleMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.darwin.kt deleted file mode 100644 index a879b10aaa9c9..0000000000000 --- a/lifecycle/lifecycle-viewmodel/src/appleMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.darwin.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 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.lifecycle.viewmodel.internal - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE diff --git a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt index 6517b49b545a5..ef4a4f9799c96 100644 --- a/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt +++ b/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.kt @@ -25,13 +25,7 @@ import kotlin.contracts.contract */ internal expect class SynchronizedObject() -/** - * Executes the given function [action] while holding the monitor of the given object [lock]. - * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - */ +/** Executes the given function [action] while holding the monitor of the given object [lock]. */ @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") // KT-29963 internal inline fun synchronized(lock: SynchronizedObject, crossinline action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } @@ -41,10 +35,6 @@ internal inline fun synchronized(lock: SynchronizedObject, crossinline actio /** * Executes the given function [action] while holding the monitor of the given object [lock]. * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - * * **This is a private API and should not be used from general code.** This function exists * primarily as a workaround for a Kotlin issue * ([KT-29963](https://youtrack.jetbrains.com/issue/KT-29963)). diff --git a/lifecycle/lifecycle-viewmodel/src/linuxMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.linux.kt b/lifecycle/lifecycle-viewmodel/src/linuxMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.linux.kt deleted file mode 100644 index 9224207027a6a..0000000000000 --- a/lifecycle/lifecycle-viewmodel/src/linuxMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.linux.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 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.lifecycle.viewmodel.internal - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt() diff --git a/lifecycle/lifecycle-viewmodel/src/mingwX64Main/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.mingwX64.kt b/lifecycle/lifecycle-viewmodel/src/mingwX64Main/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.mingwX64.kt deleted file mode 100644 index 49a4c184a55e7..0000000000000 --- a/lifecycle/lifecycle-viewmodel/src/mingwX64Main/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.mingwX64.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 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.lifecycle.viewmodel.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.PTHREAD_MUTEX_RECURSIVE -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_tVar -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_tVar - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = PTHREAD_MUTEX_RECURSIVE - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl { - private val arena = Arena() - private val attr: pthread_mutexattr_tVar = arena.alloc() - private val mutex: pthread_mutex_tVar = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt b/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt index 00bd2649f16cd..878b3130c6fee 100644 --- a/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt +++ b/lifecycle/lifecycle-viewmodel/src/nativeMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.kt @@ -16,52 +16,9 @@ package androidx.lifecycle.viewmodel.internal -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner +internal actual class SynchronizedObject : kotlinx.atomicfu.locks.SynchronizedObject() -/** - * Wrapper for platform.posix.PTHREAD_MUTEX_RECURSIVE which is represented as kotlin.Int on darwin - * platforms and kotlin.UInt on linuxX64 See: https://youtrack.jetbrains.com/issue/KT-41509 - */ -internal expect val PTHREAD_MUTEX_RECURSIVE: Int - -internal expect class SynchronizedObjectImpl() { - internal fun lock(): Int - - internal fun unlock(): Int - - internal fun dispose() -} - -internal actual class SynchronizedObject actual constructor() { - private val impl = SynchronizedObjectImpl() - - @Suppress("unused") // The returned Cleaner must be assigned to a property - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(impl, SynchronizedObjectImpl::dispose) - - fun lock() { - impl.lock() - } - - fun unlock() { - impl.unlock() - } -} - -@OptIn(ExperimentalContracts::class) internal actual inline fun synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T, -): T { - contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } - lock.lock() - return try { - action() - } finally { - lock.unlock() - } -} +): T = kotlinx.atomicfu.locks.synchronized(lock, action) diff --git a/lifecycle/lifecycle-viewmodel/src/unixMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.unix.kt b/lifecycle/lifecycle-viewmodel/src/unixMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.unix.kt deleted file mode 100644 index 32fa602ae21a7..0000000000000 --- a/lifecycle/lifecycle-viewmodel/src/unixMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.unix.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2025 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.lifecycle.viewmodel.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_t -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_t - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl actual constructor() { - private val arena: Arena = Arena() - private val attr: pthread_mutexattr_t = arena.alloc() - private val mutex: pthread_mutex_t = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/navigationevent/navigationevent/build.gradle b/navigationevent/navigationevent/build.gradle index 15db17df9e7b2..87a8fe63776ae 100644 --- a/navigationevent/navigationevent/build.gradle +++ b/navigationevent/navigationevent/build.gradle @@ -84,14 +84,9 @@ androidXMultiplatform { implementation(libs.testRunner) } - unixMain.dependsOn(nativeMain) - unixTest.dependsOn(nativeTest) - - appleMain.dependsOn(unixMain) - appleTest.dependsOn(unixTest) - - linuxMain.dependsOn(unixMain) - linuxTest.dependsOn(unixTest) + nativeMain.dependencies { + implementation(libs.atomicFu) + } webTest.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") diff --git a/navigationevent/navigationevent/src/appleMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.apple.kt b/navigationevent/navigationevent/src/appleMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.apple.kt deleted file mode 100644 index 797c73b56fb88..0000000000000 --- a/navigationevent/navigationevent/src/appleMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.apple.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 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.navigationevent.internal - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.kt index ed0cf9d56b952..c5a148730dae7 100644 --- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.kt +++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.kt @@ -26,13 +26,7 @@ import kotlin.contracts.contract */ internal expect class SynchronizedObject() -/** - * Executes the given function [action] while holding the monitor of the given object [lock]. - * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - */ +/** Executes the given function [action] while holding the monitor of the given object [lock]. */ @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") // KT-29963 internal inline fun synchronized(lock: SynchronizedObject, crossinline action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } @@ -42,10 +36,6 @@ internal inline fun synchronized(lock: SynchronizedObject, crossinline actio /** * Executes the given function [action] while holding the monitor of the given object [lock]. * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - * * **This is a private API and should not be used from general code.** This function exists * primarily as a workaround for a Kotlin issue * ([KT-29963](https://youtrack.jetbrains.com/issue/KT-29963)). diff --git a/navigationevent/navigationevent/src/linuxMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.linux.kt b/navigationevent/navigationevent/src/linuxMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.linux.kt deleted file mode 100644 index c8d7558aaae51..0000000000000 --- a/navigationevent/navigationevent/src/linuxMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.linux.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 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.navigationevent.internal - -import platform.posix.PTHREAD_MUTEX_RECURSIVE - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = PTHREAD_MUTEX_RECURSIVE.toInt() diff --git a/navigationevent/navigationevent/src/mingwMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.mingw.kt b/navigationevent/navigationevent/src/mingwMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.mingw.kt deleted file mode 100644 index 5b65846ece3d6..0000000000000 --- a/navigationevent/navigationevent/src/mingwMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.mingw.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 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.navigationevent.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.PTHREAD_MUTEX_RECURSIVE -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_tVar -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_tVar - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = PTHREAD_MUTEX_RECURSIVE - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl { - private val arena = Arena() - private val attr: pthread_mutexattr_tVar = arena.alloc() - private val mutex: pthread_mutex_tVar = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/navigationevent/navigationevent/src/nativeMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.kt b/navigationevent/navigationevent/src/nativeMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.kt index 8c43ffbe33c47..2f65e051ca966 100644 --- a/navigationevent/navigationevent/src/nativeMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.kt +++ b/navigationevent/navigationevent/src/nativeMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.kt @@ -16,52 +16,9 @@ package androidx.navigationevent.internal -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner +internal actual class SynchronizedObject : kotlinx.atomicfu.locks.SynchronizedObject() -/** - * Wrapper for platform.posix.PTHREAD_MUTEX_RECURSIVE which is represented as kotlin.Int on darwin - * platforms and kotlin.UInt on linuxX64 See: https://youtrack.jetbrains.com/issue/KT-41509 - */ -internal expect val PTHREAD_MUTEX_RECURSIVE: Int - -internal expect class SynchronizedObjectImpl() { - internal fun lock(): Int - - internal fun unlock(): Int - - internal fun dispose() -} - -internal actual class SynchronizedObject actual constructor() { - private val impl = SynchronizedObjectImpl() - - @Suppress("unused") // The returned Cleaner must be assigned to a property - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(impl, SynchronizedObjectImpl::dispose) - - fun lock() { - impl.lock() - } - - fun unlock() { - impl.unlock() - } -} - -@OptIn(ExperimentalContracts::class) internal actual inline fun synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T, -): T { - contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } - lock.lock() - return try { - action() - } finally { - lock.unlock() - } -} +): T = kotlinx.atomicfu.locks.synchronized(lock, action) diff --git a/navigationevent/navigationevent/src/unixMain/kotlin/androidx/natigationevent/internal/SynchronizedObject.native.unix.kt b/navigationevent/navigationevent/src/unixMain/kotlin/androidx/natigationevent/internal/SynchronizedObject.native.unix.kt deleted file mode 100644 index 401a5c43ee127..0000000000000 --- a/navigationevent/navigationevent/src/unixMain/kotlin/androidx/natigationevent/internal/SynchronizedObject.native.unix.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2025 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.navigationevent.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_t -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_t - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl actual constructor() { - private val arena: Arena = Arena() - private val attr: pthread_mutexattr_t = arena.alloc() - private val mutex: pthread_mutex_t = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle index 0c7f4b33650ac..bbf255eadc1b4 100644 --- a/savedstate/savedstate/build.gradle +++ b/savedstate/savedstate/build.gradle @@ -78,14 +78,9 @@ androidXMultiplatform { desktopMain.dependsOn(nonAndroidMain) desktopTest.dependsOn(nonAndroidTest) - unixMain.dependsOn(nativeMain) - unixTest.dependsOn(nativeTest) - - appleMain.dependsOn(unixMain) - appleTest.dependsOn(unixTest) - - linuxMain.dependsOn(unixMain) - linuxTest.dependsOn(unixTest) + nativeMain.dependencies { + implementation(libs.atomicFu) + } webTest.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") diff --git a/savedstate/savedstate/src/appleMain/kotlin/androidx/savedstate/internal/SynchronizedObject.darwin.kt b/savedstate/savedstate/src/appleMain/kotlin/androidx/savedstate/internal/SynchronizedObject.darwin.kt deleted file mode 100644 index 76d5b329bc9d8..0000000000000 --- a/savedstate/savedstate/src/appleMain/kotlin/androidx/savedstate/internal/SynchronizedObject.darwin.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2024 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.savedstate.internal - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE diff --git a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SynchronizedObject.kt b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SynchronizedObject.kt index 0000131f12080..8813d5b363344 100644 --- a/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SynchronizedObject.kt +++ b/savedstate/savedstate/src/commonMain/kotlin/androidx/savedstate/internal/SynchronizedObject.kt @@ -28,13 +28,7 @@ import kotlin.contracts.contract */ internal expect class SynchronizedObject() -/** - * Executes the given function [action] while holding the monitor of the given object [lock]. - * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - */ +/** Executes the given function [action] while holding the monitor of the given object [lock]. */ @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") // KT-29963 internal inline fun synchronized(lock: SynchronizedObject, crossinline action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } @@ -44,10 +38,6 @@ internal inline fun synchronized(lock: SynchronizedObject, crossinline actio /** * Executes the given function [action] while holding the monitor of the given object [lock]. * - * The implementation is platform specific: - * - JVM: implemented via `synchronized`, `ReentrantLock` is avoided for performance reasons. - * - Native: implemented via POSIX mutex with `PTHREAD_MUTEX_RECURSIVE` flag. - * * **This is a private API and should not be used from general code.** This function exists * primarily as a workaround for a Kotlin issue * ([KT-29963](https://youtrack.jetbrains.com/issue/KT-29963)). diff --git a/savedstate/savedstate/src/linuxMain/kotlin/androidx/savedstate/internal/SynchronizedObject.linux.kt b/savedstate/savedstate/src/linuxMain/kotlin/androidx/savedstate/internal/SynchronizedObject.linux.kt deleted file mode 100644 index 758f04da8047d..0000000000000 --- a/savedstate/savedstate/src/linuxMain/kotlin/androidx/savedstate/internal/SynchronizedObject.linux.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 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.savedstate.internal - -import platform.posix.PTHREAD_MUTEX_RECURSIVE - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = PTHREAD_MUTEX_RECURSIVE.toInt() diff --git a/savedstate/savedstate/src/mingwX64Main/kotlin/androidx/savedstate/internal/SynchronizedObject.native.mingwX64.kt b/savedstate/savedstate/src/mingwX64Main/kotlin/androidx/savedstate/internal/SynchronizedObject.native.mingwX64.kt deleted file mode 100644 index 763e95874470c..0000000000000 --- a/savedstate/savedstate/src/mingwX64Main/kotlin/androidx/savedstate/internal/SynchronizedObject.native.mingwX64.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 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.savedstate.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.PTHREAD_MUTEX_RECURSIVE -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_tVar -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_tVar - -internal actual val PTHREAD_MUTEX_RECURSIVE: Int = PTHREAD_MUTEX_RECURSIVE - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl { - private val arena = Arena() - private val attr: pthread_mutexattr_tVar = arena.alloc() - private val mutex: pthread_mutex_tVar = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/savedstate/savedstate/src/nativeMain/kotlin/androidx/savedstate/internal/SynchronizedObject.native.kt b/savedstate/savedstate/src/nativeMain/kotlin/androidx/savedstate/internal/SynchronizedObject.native.kt index a1a56e1b02310..8831b02233578 100644 --- a/savedstate/savedstate/src/nativeMain/kotlin/androidx/savedstate/internal/SynchronizedObject.native.kt +++ b/savedstate/savedstate/src/nativeMain/kotlin/androidx/savedstate/internal/SynchronizedObject.native.kt @@ -16,52 +16,9 @@ package androidx.savedstate.internal -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.ref.createCleaner +internal actual class SynchronizedObject : kotlinx.atomicfu.locks.SynchronizedObject() -/** - * Wrapper for platform.posix.PTHREAD_MUTEX_RECURSIVE which is represented as kotlin.Int on darwin - * platforms and kotlin.UInt on linuxX64 See: https://youtrack.jetbrains.com/issue/KT-41509 - */ -internal expect val PTHREAD_MUTEX_RECURSIVE: Int - -internal expect class SynchronizedObjectImpl() { - internal fun lock(): Int - - internal fun unlock(): Int - - internal fun dispose() -} - -internal actual class SynchronizedObject actual constructor() { - private val impl = SynchronizedObjectImpl() - - @Suppress("unused") // The returned Cleaner must be assigned to a property - @OptIn(ExperimentalNativeApi::class) - private val cleaner = createCleaner(impl, SynchronizedObjectImpl::dispose) - - fun lock() { - impl.lock() - } - - fun unlock() { - impl.unlock() - } -} - -@OptIn(ExperimentalContracts::class) internal actual inline fun synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T, -): T { - contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } - lock.lock() - return try { - action() - } finally { - lock.unlock() - } -} +): T = kotlinx.atomicfu.locks.synchronized(lock, action) diff --git a/savedstate/savedstate/src/unixMain/kotlin/androidx/savedstate/internal/SynchronizedObject.unix.kt b/savedstate/savedstate/src/unixMain/kotlin/androidx/savedstate/internal/SynchronizedObject.unix.kt deleted file mode 100644 index 00b19087a2e46..0000000000000 --- a/savedstate/savedstate/src/unixMain/kotlin/androidx/savedstate/internal/SynchronizedObject.unix.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2025 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.savedstate.internal - -import kotlinx.cinterop.Arena -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.alloc -import kotlinx.cinterop.ptr -import platform.posix.pthread_mutex_destroy -import platform.posix.pthread_mutex_init -import platform.posix.pthread_mutex_lock -import platform.posix.pthread_mutex_t -import platform.posix.pthread_mutex_unlock -import platform.posix.pthread_mutexattr_destroy -import platform.posix.pthread_mutexattr_init -import platform.posix.pthread_mutexattr_settype -import platform.posix.pthread_mutexattr_t - -@OptIn(ExperimentalForeignApi::class) -internal actual class SynchronizedObjectImpl actual constructor() { - private val arena: Arena = Arena() - private val attr: pthread_mutexattr_t = arena.alloc() - private val mutex: pthread_mutex_t = arena.alloc() - - init { - pthread_mutexattr_init(attr.ptr) - pthread_mutexattr_settype(attr.ptr, PTHREAD_MUTEX_RECURSIVE) - pthread_mutex_init(mutex.ptr, attr.ptr) - } - - internal actual fun lock(): Int = pthread_mutex_lock(mutex.ptr) - - internal actual fun unlock(): Int = pthread_mutex_unlock(mutex.ptr) - - internal actual fun dispose() { - pthread_mutex_destroy(mutex.ptr) - pthread_mutexattr_destroy(attr.ptr) - arena.clear() - } -} diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimateTick.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimateTick.kt index 82c71a45174af..bcfdfa37ed38d 100644 --- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimateTick.kt +++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AnimateTick.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultBlendMode import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.wear.compose.materialcore.SelectionStage @@ -53,6 +55,69 @@ public fun DrawScope.animateTick( } } +/** + * Draws the provided tick mark that scales in/out in size. The color's alpha component controls the + * fade in/out. The scaleProgress controls the size, from 0.0 (no size) to 1.0 (full size), with a + * Cubic Ease-Out curve applied to the scaling. This means the tick grows quickly at the start of + * the animation and slows down as it reaches full size. + */ +internal fun DrawScope.drawScalingTick( + tickPath: Path, + tickColor: Color, + scaleProgress: Float, + enabled: Boolean, +) { + // Optimization: Don't draw if completely transparent or scaled to zero + if (tickColor.alpha == 0f || scaleProgress == 0f) { + return + } + + val strokeWidth = TICK_STROKE_WIDTH_DP.toPx() + val tickDesignCenterX = TICK_DESIGN_CENTER_X_DP.toPx() + val tickDesignCenterY = TICK_DESIGN_CENTER_Y_DP.toPx() + + val pivotOffset = Offset(tickDesignCenterX, tickDesignCenterY) + + val normalizedProgress = scaleProgress.coerceIn(0f, 1f) + // Apply a Cubic Ease-Out function: increase quickly at the beginning and slow down towards the + // end for better tick visibility + val easedScaleFactor = 1f - (1f - normalizedProgress).pow(3) + + // Scale around the tick's design center. + scale(scale = easedScaleFactor, pivot = pivotOffset) { + drawPath( + path = tickPath, + color = tickColor, + style = Stroke(width = strokeWidth, cap = StrokeCap.Round), + blendMode = if (enabled) DefaultBlendMode else BlendMode.Hardlight, + ) + } +} + +/** + * Creates a [Path] object representing the complete geometry of the checkmark. The path coordinates + * are defined based on fitting the icon within a 24.dp x 24.dp container, and are converted to + * pixels using the receiver [Density]. + */ +internal fun Density.createFullTickPath(): Path { + val tickBaseComponent = TICK_BASE_COMPONENT_DP.toPx() + val tickStickComponent = TICK_STICK_COMPONENT_DP.toPx() + + val baseStartX = BASE_START_X_DP.toPx() + val baseStartY = BASE_START_Y_DP.toPx() + val stickStartX = STICK_START_X_DP.toPx() + val stickStartY = STICK_START_Y_DP.toPx() + + return Path().apply { + // Base segment + moveTo(baseStartX, baseStartY) + lineTo(baseStartX + tickBaseComponent, baseStartY + tickBaseComponent) + // Stick segment + moveTo(stickStartX, stickStartY) + lineTo(stickStartX + tickStickComponent, stickStartY - tickStickComponent) + } +} + private fun DrawScope.drawTick( tickColor: Color, tickProgress: Float, @@ -61,12 +126,13 @@ private fun DrawScope.drawTick( ) { // Using tickProgress animating from zero to TICK_TOTAL_LENGTH, // rotate the tick as we draw from 15 degrees to zero. - val tickBaseLength = TICK_BASE_LENGTH.toPx() - val tickStickLength = TICK_STICK_LENGTH.toPx() + val tickBaseLength = TICK_BASE_COMPONENT_DP.toPx() + val tickStickLength = TICK_STICK_COMPONENT_DP.toPx() val tickTotalLength = tickBaseLength + tickStickLength val tickProgressPx = tickProgress * tickTotalLength val startXOffsetPx = startXOffset.toPx() - val center = Offset(12.dp.toPx() + startXOffsetPx, 12.dp.toPx()) + val center = + Offset(TICK_DESIGN_CENTER_X_DP.toPx() + startXOffsetPx, TICK_DESIGN_CENTER_Y_DP.toPx()) // Normalized progress for angle calculation (0 to 1) val normalizedProgress = tickProgress.coerceIn(0f, 1f) @@ -83,7 +149,7 @@ private fun DrawScope.drawTick( val angleRadians = angle.toRadians() // Animate the base of the tick. - val baseStart = Offset(7.4f.dp.toPx() + startXOffsetPx, 13.0f.dp.toPx()) + val baseStart = Offset(BASE_START_X_DP.toPx() + startXOffsetPx, BASE_START_Y_DP.toPx()) val tickBaseProgress = min(tickProgressPx, tickBaseLength) val path = Path() @@ -94,7 +160,7 @@ private fun DrawScope.drawTick( if (tickProgressPx > tickBaseLength) { val tickStickProgress = min(tickProgressPx - tickBaseLength, tickStickLength) - val stickStart = Offset(10.5f.dp.toPx() + startXOffsetPx, 15.1f.dp.toPx()) + val stickStart = Offset(STICK_START_X_DP.toPx() + startXOffsetPx, STICK_START_Y_DP.toPx()) // Move back to the start of the stick (without drawing) path.moveTo(stickStart.rotate(angleRadians, center)) path.lineTo( @@ -105,7 +171,7 @@ private fun DrawScope.drawTick( drawPath( path, tickColor, - style = Stroke(width = 2.dp.toPx(), cap = StrokeCap.Round), + style = Stroke(width = TICK_STROKE_WIDTH_DP.toPx(), cap = StrokeCap.Round), blendMode = if (enabled) DefaultBlendMode else BlendMode.Hardlight, ) } @@ -116,8 +182,8 @@ private fun DrawScope.eraseTick( startXOffset: Dp, enabled: Boolean, ) { - val tickBaseLength = TICK_BASE_LENGTH.toPx() - val tickStickLength = TICK_STICK_LENGTH.toPx() + val tickBaseLength = TICK_BASE_COMPONENT_DP.toPx() + val tickStickLength = TICK_STICK_COMPONENT_DP.toPx() val tickTotalLength = tickBaseLength + tickStickLength val tickProgressPx = tickProgress * tickTotalLength val startXOffsetPx = startXOffset.toPx() @@ -143,7 +209,7 @@ private fun DrawScope.eraseTick( drawPath( path, tickColor, - style = Stroke(width = 2.dp.toPx(), cap = StrokeCap.Round), + style = Stroke(width = TICK_STROKE_WIDTH_DP.toPx(), cap = StrokeCap.Round), blendMode = if (enabled) DefaultBlendMode else BlendMode.Hardlight, ) } @@ -170,6 +236,20 @@ public fun directionVector(angleRadians: Float): Offset = private fun Offset.rotate90() = Offset(-y, x) -private val TICK_BASE_LENGTH = 2.5.dp -private val TICK_STICK_LENGTH = 6.dp +// These COMPONENT constants represent the equal horizontal and vertical projections +// of the 45-degree line segments. The actual Euclidean length of the segments +// is sqrt(2) * component_value. +private val TICK_BASE_COMPONENT_DP = 2.5.dp // dX and dY for the base segment +private val TICK_STICK_COMPONENT_DP = 6.dp // dX and dY for the stick segment + +private val BASE_START_X_DP = 7.4.dp +private val BASE_START_Y_DP = 13.0.dp +private val STICK_START_X_DP = 10.5.dp +private val STICK_START_Y_DP = 15.1f.dp + +// Center of the tick's 24.dp design box +private val TICK_DESIGN_CENTER_X_DP = 12.dp +private val TICK_DESIGN_CENTER_Y_DP = 12.dp + +private val TICK_STROKE_WIDTH_DP = 2.dp private const val TICK_ROTATION = 15f diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt index 1ad9c780b31d8..f79d98fd8038b 100644 --- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt +++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/SwitchButton.kt @@ -48,12 +48,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.takeOrElse @@ -1861,48 +1864,55 @@ private fun Switch( .semantics { this.role = Role.Switch } .height(SWITCH_INNER_HEIGHT) .width(SWITCH_WIDTH) - .drawBehind { - val currentThumbColor = actualThumbColor.value - val currentThumbIconColor = actualThumbIconColor.value - val currentTrackColor = actualTrackColor.value - val currentTrackBorderColor = actualTrackBorderColor.value - - // Draw track background - drawRoundRect( - color = currentTrackColor, - size = size, - cornerRadius = CornerRadius(size.height / 2), - ) - - // Draw track border - val borderColor = - if (currentTrackColor == currentTrackBorderColor) { - Color.Transparent - } else { - currentTrackBorderColor - } - - val strokeWidthPx = SWITCH_TRACK_WIDTH.toPx() - // Inset the drawing area for the border by half the stroke width to replicate - // Modifier.border's inset behavior. - val inset = strokeWidthPx / 2 - drawRoundRect( - color = borderColor, - topLeft = Offset(inset, inset), - size = Size(size.width - strokeWidthPx, size.height - strokeWidthPx), - cornerRadius = CornerRadius((size.height - strokeWidthPx) / 2f), - style = androidx.compose.ui.graphics.drawscope.Stroke(width = strokeWidthPx), - ) - - // Draw thumb and tick on top - drawThumbAndTick( - enabled, - checked, - currentThumbColor, - thumbProgress.value, - currentThumbIconColor, - isRtl, - ) + .drawWithCache { + val tickPath = createFullTickPath() // Avoid recreating the Path on every frame + + onDrawBehind { // This block is run on every invalidation of the draw phase + val currentThumbColor = actualThumbColor.value + val currentThumbIconColor = actualThumbIconColor.value + val currentTrackColor = actualTrackColor.value + val currentTrackBorderColor = actualTrackBorderColor.value + + // Draw track background + drawRoundRect( + color = currentTrackColor, + size = size, + cornerRadius = CornerRadius(size.height / 2), + ) + + // Draw track border + val borderColor = + if (currentTrackColor == currentTrackBorderColor) { + Color.Transparent + } else { + currentTrackBorderColor + } + + val strokeWidthPx = SWITCH_TRACK_WIDTH.toPx() + // Inset the drawing area for the border by half the stroke width to + // replicate + // Modifier.border's inset behavior. + val inset = strokeWidthPx / 2 + drawRoundRect( + color = borderColor, + topLeft = Offset(inset, inset), + size = Size(size.width - strokeWidthPx, size.height - strokeWidthPx), + cornerRadius = CornerRadius((size.height - strokeWidthPx) / 2f), + style = + androidx.compose.ui.graphics.drawscope.Stroke(width = strokeWidthPx), + ) + + // Draw thumb and tick on top + drawThumbAndTick( + enabled, + checked, + currentThumbColor, + thumbProgress.value, + currentThumbIconColor, + isRtl, + tickPath, + ) + } } .wrapContentSize(Alignment.CenterEnd) ) @@ -1915,6 +1925,7 @@ private fun DrawScope.drawThumbAndTick( progress: Float, thumbIconColor: Color, isRtl: Boolean, + tickPath: Path, ) { val thumbPaddingUnchecked = SWITCH_INNER_HEIGHT / 2 - THUMB_RADIUS_UNCHECKED @@ -1950,25 +1961,21 @@ private fun DrawScope.drawThumbAndTick( center = Offset(thumbProgressPx, center.y), ) - val ltrAdditionalOffset = 5.dp.toPx() - val rtlAdditionalOffset = 6.dp.toPx() - - val totalDist = switchTrackLengthPx - 2 * switchThumbRadiusPx - ltrAdditionalOffset - - // Offset value to be added if RTL mode is enabled. - // We need to move the tick to the checked position in ltr mode when unchecked. - val rtlOffset = switchTrackLengthPx - 2 * THUMB_RADIUS_CHECKED.toPx() - rtlAdditionalOffset - - val distMoved = if (isRtl) rtlOffset - progress * totalDist else progress * totalDist - - // Draw tick icon - animateTick( - enabled = enabled, - checked = checked, - tickColor = thumbIconColor, - tickProgress = progress, - startXOffset = distMoved.toDp(), - ) + // Center of the tick's design, in pixels. + val tickDesignCenterX = 12.dp.toPx() + val tickDesignCenterY = 12.dp.toPx() + + // Translate the canvas so the tick's design center (12.dp, 12.dp) + // aligns with the thumb's current center (thumbProgressPx, center.y). + translate(left = thumbProgressPx - tickDesignCenterX, top = center.y - tickDesignCenterY) { + // Call the new scaling tick function from AnimateTick.kt + drawScalingTick( + tickPath = tickPath, + tickColor = thumbIconColor, + scaleProgress = progress, + enabled = enabled, + ) + } } @Composable diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/animation/SampleAnimations.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/animation/SampleAnimations.kt index 90e2223a619e5..41c23cc764f1f 100644 --- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/animation/SampleAnimations.kt +++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/animation/SampleAnimations.kt @@ -63,7 +63,7 @@ class SampleAnimations : ComponentActivity() { val (animationStyle, setAnimationStyle) = remember { mutableStateOf(AnimationStyle.SequentialExample) } MainPanelContent(setAnimationStyle) - Subspace { + Subspace(allowUnboundedSubspace = true) { SpatialCurvedRow(modifier = SubspaceModifier.fillMaxSize(), curveRadius = 1025.dp) { SpatialMainPanel(modifier = SubspaceModifier.width(600.dp).height(400.dp)) SpatialColumn(modifier = SubspaceModifier.padding(50.dp)) { diff --git a/xr/runtime/runtime/api/current.txt b/xr/runtime/runtime/api/current.txt index 00d61c9839160..a2fbe9d3fd039 100644 --- a/xr/runtime/runtime/api/current.txt +++ b/xr/runtime/runtime/api/current.txt @@ -142,6 +142,10 @@ package androidx.xr.runtime { property public kotlin.time.ComparableTimeMark timeMark; } + @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental API. It may be changed or removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalXrDeviceLifecycleApi { + ctor @KotlinOnly public ExperimentalXrDeviceLifecycleApi(); + } + public final class FieldOfView { ctor public FieldOfView(float angleLeft, float angleRight, float angleUp, float angleDown); method public androidx.xr.runtime.FieldOfView copy(); diff --git a/xr/runtime/runtime/api/restricted_current.txt b/xr/runtime/runtime/api/restricted_current.txt index cc5b91dca76c5..d64fd6def8f25 100644 --- a/xr/runtime/runtime/api/restricted_current.txt +++ b/xr/runtime/runtime/api/restricted_current.txt @@ -197,6 +197,10 @@ package androidx.xr.runtime { property public kotlin.time.ComparableTimeMark timeMark; } + @SuppressCompatibility @kotlin.RequiresOptIn(message="This is an experimental API. It may be changed or removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalXrDeviceLifecycleApi { + ctor @KotlinOnly public ExperimentalXrDeviceLifecycleApi(); + } + public final class FieldOfView { ctor public FieldOfView(float angleLeft, float angleRight, float angleUp, float angleDown); method public androidx.xr.runtime.FieldOfView copy(); diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ExperimentalXrDeviceLifecycleApi.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ExperimentalXrDeviceLifecycleApi.kt new file mode 100644 index 0000000000000..e374a5d32ef6f --- /dev/null +++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ExperimentalXrDeviceLifecycleApi.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 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.xr.runtime + +/** + * Marks XrDevice lifecycle APIs that are experimental and likely to change or be removed in the + * future. + * + * Any usage of a declaration annotated with `@ExperimentalXrDeviceLifecycleApi` must be accepted + * either by annotating that usage with `@OptIn(ExperimentalXrDeviceLifecycleApi::class)` or by + * propagating the annotation to the containing declaration. + */ +@RequiresOptIn(message = "This is an experimental API. It may be changed or removed in the future.") +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalXrDeviceLifecycleApi()