From cb489d166b5fa249a52c3d100a46b5acacd740d8 Mon Sep 17 00:00:00 2001 From: Michal Idzkowski Date: Thu, 8 Jan 2026 11:17:51 -0500 Subject: [PATCH 01/12] Adds the `ExperimentalXrDeviceLifecycleApi` annotation. Bug: 474095480 Test: Presubmit Relnote: Added the `ExperimentalXrDeviceLifecycleApi` annotation Change-Id: I7f54e2301fbe2c5792a64385d76f0af98a23c504 --- xr/runtime/runtime/api/current.txt | 4 +++ xr/runtime/runtime/api/restricted_current.txt | 4 +++ .../ExperimentalXrDeviceLifecycleApi.kt | 29 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/ExperimentalXrDeviceLifecycleApi.kt 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 91b12f1f42c66..f5a639c90f7fc 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() From c39f331c376c84d05ff6b94d119c4c3114aaa6fa Mon Sep 17 00:00:00 2001 From: Sravani Vadlamani Date: Sat, 10 Jan 2026 13:05:06 +0000 Subject: [PATCH 02/12] Fix: SwitchButton tick to scale/fade as a complete shape Implements the correct motion spec for the Tick icon in SwitchButton (Wear Compose Material3). The previous animation, using animateTick(), drew the tick in segments. The new design spec requires the complete tick icon to scale up from the center of the thumb and fade in. Key changes: 1. AnimateTick.kt; New internal functions added: * Density.createFullTickPath(): Creates and returns a Path object for the complete tick, that fits inside a 24.dp*24.dp container and using Density for pixel conversions. * DrawScope.drawScalingTick(): This function applies a scale transformation to the provided Path. A Cubic Ease-Out easing curve is used on the scale factor for better visibility. 2. SwitchButton.kt Update: * Added Modifier.drawWithCache to efficiently cache the fully-drawn tick Path object generated via createFullTickPath(). * The onDrawBehind{} lambda within drawWithCache now contains the original drawing logic. * drawThumbAndTick() now accepts the cached Path, uses DrawScope.translate to center the drawing context on the thumb, and then calls drawScalingTick() to render the scaled tick. 3. Nomenclature Cleanup: Renamed constants in AnimateTick.kt (e.g., TICK_BASE_LENGTH_DP to TICK_BASE_COMPONENT_DP) to more accurately reflect that they represent vector components rather than Euclidean lengths, and added comments to clarify. These changes ensure the SwitchButton tick animation aligns with the UX motion specification and improve performance by caching the fully-formed tick Path. Bug: 378644361 Fixes: 378644361 Test: Manually verified the animation in the Wear Compose Demos app. The tick now scales and fades in smoothly from the center of the thumb as expected. Change-Id: I7a7eb896ce9b1a693150b05ff84399eb62c75770 --- .../wear/compose/material3/AnimateTick.kt | 102 ++++++++++++-- .../wear/compose/material3/SwitchButton.kt | 129 +++++++++--------- 2 files changed, 159 insertions(+), 72 deletions(-) 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 From 80064bd8d253ff212bd70c31e8c65b1c939e92b4 Mon Sep 17 00:00:00 2001 From: Yashwanth Gajji Date: Mon, 12 Jan 2026 11:49:58 -0800 Subject: [PATCH 03/12] Unbound subspace for animation testcase Bug: 473600344 Test: Tested in Aura emulator Change-Id: I91dcf3068cb48326efae46c62b17b0b6e57f22d6 --- .../androidx/xr/compose/testapp/animation/SampleAnimations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) { From b949bab3c8c8f1f6037dd139f0e86d1c7301822c Mon Sep 17 00:00:00 2001 From: Rahul Ravikumar Date: Mon, 12 Jan 2026 12:35:39 -0800 Subject: [PATCH 04/12] Add partial support for Kotlin Multiplatform Android Library Modules. Test: Added unit tests. Relnote: "Typically dependencies can be defined using the `baselineProfile` Gradle configuration. However that does not seem to work for the `androidMain.dependencies` block. ```kotlin kotlin { androidLibrary { namespace = \"com.example.namespace\" compileSdk = 36 } sourceSets { androidMain.dependencies { // THIS DOES NOT WORK // baselineProfile(\":yourProducerProject\") } } } ``` Instead you should be defining your dependencies using the `baselineProfile` extension. ``` plugins { id(\"org.jetbrains.kotlin.multiplatform\") id(\"com.android.kotlin.multiplatform.library\") id(\"androidx.baselineprofile.consumer\") } kotlin { androidLibrary { namespace = \"com.example.namespace\" compileSdk = 36 } sourceSets { androidMain.dependencies { // ... } } } // Define dependencies // This works ! baselineProfile { variants { androidMain { from(project(\":yourProducerProject\")) } } } ``` " Change-Id: Ie19c4535ebe25ead09e22d9a5e7654c2a5d0b627 --- .../consumer/BaselineProfileConsumerPlugin.kt | 70 ++++++++++++++----- .../baselineprofile/gradle/utils/AgpPlugin.kt | 26 +++++-- .../baselineprofile/gradle/utils/Constants.kt | 3 + .../wrapper/BaselineProfileWrapperPlugin.kt | 7 ++ ...neProfileKotlinMultiplatformLibraryTest.kt | 70 +++++++++++++++++++ .../utils/BaselineProfileProjectSetupRule.kt | 41 +++++++++++ .../baselineprofile/gradle/utils/Constants.kt | 10 +-- 7 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 benchmark/baseline-profile-gradle-plugin/src/test/kotlin/androidx/baselineprofile/gradle/consumer/BaselineProfileKotlinMultiplatformLibraryTest.kt 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 } } } From dc694373dd2d9937a6d6e1628a4c58d990becaa6 Mon Sep 17 00:00:00 2001 From: Jasmine Chen Date: Fri, 9 Jan 2026 16:14:34 -0800 Subject: [PATCH 05/12] Introduce StrictMode for checking non-compliant behaviors Introduce a dedicated StrictMode for checking non-compliant behaviors. Bug: 356268566 Test: Unit tests Change-Id: I0bf87b75ad3967903c2b7f3d6072b6d202410c85 --- .../camera/camera2/pipe/StrictMode.kt | 34 ++++++++++++ .../camera2/pipe/compat/Camera2Quirks.kt | 35 +++++++----- .../pipe/config/CameraPipeComponent.kt | 5 ++ .../compat/Camera2CameraControllerTest.kt | 4 +- .../pipe/compat/Camera2DeviceManagerTest.kt | 6 +-- .../camera2/pipe/compat/Camera2QuirksTest.kt | 54 ++++++------------- .../compat/RetryingCameraStateOpenerTest.kt | 4 +- .../camera2/pipe/compat/VirtualCameraTest.kt | 4 +- .../camera2/pipe/graph/GraphProcessorTest.kt | 4 +- .../internal/CameraGraphListenersImplTest.kt | 4 +- .../internal/CameraGraphParametersImplTest.kt | 4 +- 11 files changed, 91 insertions(+), 67 deletions(-) create mode 100644 camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StrictMode.kt 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/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/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..ae2964770a468 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() 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/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))) From 92e9ca17d7120a8c5179ff4d7668f356355ff2e9 Mon Sep 17 00:00:00 2001 From: Jasmine Chen Date: Thu, 8 Jan 2026 13:56:42 -0800 Subject: [PATCH 06/12] Turn sequenceId check into a warning On non-compliant platforms, the sequence ID may be incorrect under extension sessions. Turn the sequenceId check into a warning. Bug: 356268566 Test: Unit tests Change-Id: I0d303fdf7f6448bb1f18c45da9a41c68e93ef79d --- .../camera/camera2/pipe/compat/Camera2CameraController.kt | 3 +++ .../camera/camera2/pipe/compat/Camera2CaptureSequence.kt | 6 ++++-- .../pipe/compat/Camera2CaptureSequenceProcessor.kt | 5 +++++ .../camera/camera2/pipe/compat/CaptureSessionState.kt | 2 ++ .../camera2/pipe/compat/Camera2CameraControllerTest.kt | 1 + .../pipe/compat/Camera2CaptureSequenceProcessorTest.kt | 6 ++++++ .../camera2/pipe/compat/Camera2CaptureSequenceTest.kt | 3 +++ .../camera2/pipe/compat/CaptureSessionFactoryTest.kt | 2 ++ .../camera/camera2/pipe/compat/CaptureSessionStateTest.kt | 8 ++++++++ 9 files changed, 34 insertions(+), 2 deletions(-) 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/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/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 ae2964770a468..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 @@ -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/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, ) From f47124cbe723c4d15ae699dcaf9feeb5d859c2fa Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Mon, 12 Jan 2026 18:30:19 +0000 Subject: [PATCH 07/12] Fix typo of dontwarn FingerprintManager. Test: ./gradlew biometric:biometric:test Change-Id: Ic9635774aab8c3878b92239a7bd49cfc19ccea05 --- biometric/biometric/proguard-rules.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ad652e91a56012fbaf8ad071f7d8049f0de61b37 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 12 Jan 2026 18:59:28 +0000 Subject: [PATCH 08/12] Migrate SynchronizedObject to AtomicFU in NavEvent Replaces the custom synchronization implementation with kotlinx-atomicfu in the native source set. Bug: N/A Test: existing passes Change-Id: I5c1f9a810fefa2829cc49b76918de565d712d0de --- navigationevent/navigationevent/build.gradle | 11 +--- .../internal/SynchronizedObject.apple.kt | 19 ------- .../internal/SynchronizedObject.kt | 12 +--- .../SynchronizedObject.native.linux.kt | 21 ------- .../SynchronizedObject.native.mingw.kt | 57 ------------------- .../internal/SynchronizedObject.native.kt | 47 +-------------- .../SynchronizedObject.native.unix.kt | 54 ------------------ 7 files changed, 6 insertions(+), 215 deletions(-) delete mode 100644 navigationevent/navigationevent/src/appleMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.apple.kt delete mode 100644 navigationevent/navigationevent/src/linuxMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.linux.kt delete mode 100644 navigationevent/navigationevent/src/mingwMain/kotlin/androidx/navigationevent/internal/SynchronizedObject.native.mingw.kt delete mode 100644 navigationevent/navigationevent/src/unixMain/kotlin/androidx/natigationevent/internal/SynchronizedObject.native.unix.kt 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() - } -} From 82edeb231b9e770cff45a459247fc507a66d59ce Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Mon, 12 Jan 2026 21:43:57 +0000 Subject: [PATCH 09/12] Update compileSdk for biometric/ to 36.1 Bug: 400115331 Test: ./gradlew biometric:biometric:assemble Test: ./gradlew :biometric:integration-tests:testapp-compose:checkDebugAarMetadata Change-Id: I9da037a654393c4c7e9eb3cdbc808a4d51bd69d5 --- biometric/biometric-compose/build.gradle | 2 +- biometric/biometric-compose/samples/build.gradle | 2 +- biometric/biometric/build.gradle | 2 +- biometric/biometric/samples/build.gradle | 2 +- biometric/integration-tests/testapp-compose/build.gradle | 2 +- biometric/integration-tests/testapp/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/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/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/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" } From a59401504bb70c910ef6173dade9994947f0a2d9 Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Sat, 10 Jan 2026 00:08:51 +0000 Subject: [PATCH 10/12] Add KeyAgreement for BiometricPrompt#CryptoObject. Bug: 400115331 Test: ./gradlew biometric:biometric:test Relnote: "CryptoObject support for KeyAgreement" Change-Id: I7302920f722bba7a360efb93fcdae74274d710ef --- biometric/biometric/api/current.txt | 2 + .../biometric/api/restricted_current.txt | 2 + .../androidx/biometric/BiometricPrompt.java | 34 ++++++++++- .../biometric/utils/CryptoObjectUtils.java | 58 ++++++++++++++++++- .../utils/CryptoObjectUtilsTest.java | 34 +++++++++++ 5 files changed, 128 insertions(+), 2 deletions(-) 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/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(); From d8f9914ccd0358a018d15a2a575b93e5e0babd1b Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 12 Jan 2026 19:03:19 +0000 Subject: [PATCH 11/12] Migrate SynchronizedObject to AtomicFU in ViewModel Replaces the custom synchronization implementation with kotlinx-atomicfu in the native source set. Bug: N/A Test: existing passes Change-Id: Ia1d860d925298286d56707ae2e7442975b7ab064 --- lifecycle/lifecycle-viewmodel/build.gradle | 6 +- .../SynchronizedObject.native.darwin.kt | 19 ------- .../viewmodel/internal/SynchronizedObject.kt | 12 +--- .../SynchronizedObject.native.linux.kt | 19 ------- .../SynchronizedObject.native.mingwX64.kt | 57 ------------------- .../internal/SynchronizedObject.native.kt | 47 +-------------- .../SynchronizedObject.native.unix.kt | 54 ------------------ 7 files changed, 6 insertions(+), 208 deletions(-) delete mode 100644 lifecycle/lifecycle-viewmodel/src/appleMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.darwin.kt delete mode 100644 lifecycle/lifecycle-viewmodel/src/linuxMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.linux.kt delete mode 100644 lifecycle/lifecycle-viewmodel/src/mingwX64Main/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.mingwX64.kt delete mode 100644 lifecycle/lifecycle-viewmodel/src/unixMain/kotlin/androidx/lifecycle/viewmodel/internal/SynchronizedObject.native.unix.kt 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() - } -} From 71c5f8864a0d34f97103b9cf124476e66557e4bc Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Mon, 12 Jan 2026 19:05:40 +0000 Subject: [PATCH 12/12] Migrate SynchronizedObject to AtomicFU in SavedState Replaces the custom synchronization implementation with kotlinx-atomicfu in the native source set. Bug: N/A Test: existing passes Change-Id: I6d73f1468c4c37c6c4d74d238833d9f268769526 --- savedstate/savedstate/build.gradle | 11 +--- .../internal/SynchronizedObject.darwin.kt | 19 ------- .../savedstate/internal/SynchronizedObject.kt | 12 +--- .../internal/SynchronizedObject.linux.kt | 21 ------- .../SynchronizedObject.native.mingwX64.kt | 57 ------------------- .../internal/SynchronizedObject.native.kt | 47 +-------------- .../internal/SynchronizedObject.unix.kt | 54 ------------------ 7 files changed, 6 insertions(+), 215 deletions(-) delete mode 100644 savedstate/savedstate/src/appleMain/kotlin/androidx/savedstate/internal/SynchronizedObject.darwin.kt delete mode 100644 savedstate/savedstate/src/linuxMain/kotlin/androidx/savedstate/internal/SynchronizedObject.linux.kt delete mode 100644 savedstate/savedstate/src/mingwX64Main/kotlin/androidx/savedstate/internal/SynchronizedObject.native.mingwX64.kt delete mode 100644 savedstate/savedstate/src/unixMain/kotlin/androidx/savedstate/internal/SynchronizedObject.unix.kt 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() - } -}