Merge "Reject value classes with non-public constructors or underlying property." into androidx-main
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index da4c9b8..f7709f6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -122,14 +122,15 @@
private val interopExtensionSessionStateCallback: CameraExtensionSession.StateCallback? = null,
private val threads: Threads
) : CameraDeviceWrapper {
+ private val closed = atomic(false)
private val _lastStateCallback = atomic<OnSessionFinalized?>(null)
override fun createCaptureSession(
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback
): Boolean {
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createCaptureSession") {
// This function was deprecated in Android Q, but is required for some
@@ -162,14 +163,15 @@
@RequiresApi(31)
override fun createExtensionSession(config: ExtensionSessionConfigData): Boolean {
- checkNotNull(config.extensionStateCallback) {
+ val stateCallback = config.extensionStateCallback
+ checkNotNull(stateCallback) {
"extensionStateCallback must be set to create Extension session"
}
checkNotNull(config.extensionMode) {
"extensionMode must be set to create Extension session"
}
- val stateCallback = config.extensionStateCallback
- val previousStateCallback = _lastStateCallback.getAndSet(stateCallback)
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createExtensionSession") {
val sessionConfig =
@@ -217,8 +219,8 @@
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback
): Boolean {
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createReprocessableCaptureSession") {
// This function was deprecated in Android Q, but is required for some
@@ -255,8 +257,8 @@
outputs: List<Surface>,
stateCallback: CameraCaptureSessionWrapper.StateCallback
): Boolean {
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createConstrainedHighSpeedCaptureSession") {
// This function was deprecated in Android Q, but is required for some
@@ -293,8 +295,8 @@
outputConfigurations: List<OutputConfigurationWrapper>,
stateCallback: CameraCaptureSessionWrapper.StateCallback
): Boolean {
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createCaptureSessionByOutputConfigurations") {
// This function was deprecated in Android Q, but is required for some
@@ -332,8 +334,8 @@
outputs: List<OutputConfigurationWrapper>,
stateCallback: CameraCaptureSessionWrapper.StateCallback
): Boolean {
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createReprocessableCaptureSessionByConfigurations") {
// This function was deprecated in Android Q, but is required for some
@@ -371,9 +373,8 @@
@RequiresApi(28)
override fun createCaptureSession(config: SessionConfigData): Boolean {
- val stateCallback = config.stateCallback
- val previousStateCallback = _lastStateCallback.value
- check(_lastStateCallback.compareAndSet(previousStateCallback, stateCallback))
+ val (success, previousStateCallback) = checkAndSetStateCallback(config.stateCallback)
+ if (!success) return false
val result =
instrumentAndCatch("createCaptureSession") {
val sessionConfig =
@@ -383,7 +384,7 @@
config.executor,
AndroidCaptureSessionStateCallback(
this,
- stateCallback,
+ config.stateCallback,
previousStateCallback,
cameraErrorListener,
interopSessionStateCallback,
@@ -476,8 +477,10 @@
}
override fun onDeviceClosed() {
- val lastStateCallback = _lastStateCallback.getAndSet(null)
- lastStateCallback?.onSessionFinalized()
+ if (closed.compareAndSet(expect = false, update = true)) {
+ val lastStateCallback = _lastStateCallback.getAndSet(null)
+ lastStateCallback?.onSessionFinalized()
+ }
}
@Suppress("UNCHECKED_CAST")
@@ -495,6 +498,16 @@
Debug.instrument("CXCP#$fnName-${cameraId.value}") {
catchAndReportCameraExceptions(cameraId, cameraErrorListener, block)
}
+
+ private fun checkAndSetStateCallback(
+ stateCallback: OnSessionFinalized
+ ): Pair<Boolean, OnSessionFinalized?> {
+ if (closed.value) {
+ stateCallback.onSessionFinalized()
+ return Pair(false, null)
+ }
+ return Pair(true, _lastStateCallback.getAndSet(stateCallback))
+ }
}
/**
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index bb7966b..1173955 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -20,7 +20,6 @@
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
-import androidx.collection.IntObjectMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
@@ -63,7 +62,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -3419,9 +3417,6 @@
override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
- override val semanticsOwner: SemanticsOwner
- get() = TODO("Not yet implemented")
-
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
@@ -3567,12 +3562,8 @@
}
override var measureIteration: Long = 0
-
override val viewConfiguration: ViewConfiguration
get() = TODO("Not yet implemented")
- override val layoutNodes: IntObjectMap<LayoutNode>
- get() = TODO("Not yet implemented")
-
override val sharedDrawScope = LayoutNodeDrawScope()
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 4c6c8245..2552b929 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -21,8 +21,6 @@
import android.view.InputDevice
import android.view.KeyEvent as AndroidKeyEvent
import android.view.MotionEvent
-import androidx.collection.IntObjectMap
-import androidx.collection.intObjectMapOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
@@ -59,8 +57,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.EmptySemanticsModifier
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -2851,9 +2847,6 @@
override val rootForTest: RootForTest
get() = TODO("Not yet implemented")
- override val layoutNodes: IntObjectMap<LayoutNode>
- get() = TODO("Not yet implemented")
-
override val hapticFeedBack: HapticFeedback
get() = TODO("Not yet implemented")
@@ -2914,9 +2907,6 @@
override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, EmptySemanticsModifier(), intObjectMapOf())
-
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 8295452..ca6ec66 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -18,8 +18,6 @@
package androidx.compose.ui.layout
-import androidx.collection.IntObjectMap
-import androidx.collection.intObjectMapOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.autofill.Autofill
@@ -54,8 +52,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.EmptySemanticsModifier
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -165,12 +161,7 @@
override fun onDetach(node: LayoutNode) {}
- override val root: LayoutNode = LayoutNode()
-
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, EmptySemanticsModifier(), intObjectMapOf())
-
- override val layoutNodes: IntObjectMap<LayoutNode>
+ override val root: LayoutNode
get() = TODO("Not yet implemented")
override val sharedDrawScope: LayoutNodeDrawScope
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index 56e499a..fd30536 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -18,8 +18,6 @@
package androidx.compose.ui.node
-import androidx.collection.IntObjectMap
-import androidx.collection.intObjectMapOf
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
@@ -50,8 +48,6 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.invertTo
-import androidx.compose.ui.semantics.EmptySemanticsModifier
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -389,9 +385,6 @@
override val density: Density
get() = Density(1f)
- override val layoutNodes: IntObjectMap<LayoutNode>
- get() = TODO("Not yet implemented")
-
override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr
@@ -428,9 +421,6 @@
override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, EmptySemanticsModifier(), intObjectMapOf())
-
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt
deleted file mode 100644
index 79bad61..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsInfoTest.kt
+++ /dev/null
@@ -1,244 +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.compose.ui.semantics
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.semantics.SemanticsProperties.TestTag
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Correspondence
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import org.junit.Rule
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class SemanticsInfoTest {
-
- @get:Rule val rule = createComposeRule()
-
- lateinit var semanticsOwner: SemanticsOwner
-
- @Test
- fun contentWithNoSemantics() {
- // Arrange.
- rule.setTestContent { Box {} }
- rule.waitForIdle()
-
- // Act.
- val rootSemantics = semanticsOwner.rootInfo
-
- // Assert.
- assertThat(rootSemantics).isNotNull()
- assertThat(rootSemantics.parentInfo).isNull()
- assertThat(rootSemantics.childrenInfo.size).isEqualTo(1)
-
- // Assert extension Functions.
- assertThat(rootSemantics.findSemanticsParent()).isNull()
- assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren()).isEmpty()
- }
-
- @Test
- fun singleSemanticsModifier() {
- // Arrange.
- rule.setTestContent { Box(Modifier.semantics { this.testTag = "testTag" }) }
- rule.waitForIdle()
-
- // Act.
- val rootSemantics = semanticsOwner.rootInfo
- val semantics = rule.getSemanticsInfoForTag("testTag")!!
-
- // Assert.
- assertThat(rootSemantics.parentInfo).isNull()
- assertThat(rootSemantics.childrenInfo.asMutableList()).containsExactly(semantics)
-
- assertThat(semantics.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics.childrenInfo.size).isEqualTo(0)
-
- // Assert extension Functions.
- assertThat(rootSemantics.findSemanticsParent()).isNull()
- assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
- .comparingElementsUsing(SemanticsConfigurationComparator)
- .containsExactly(SemanticsConfiguration().apply { testTag = "testTag" })
-
- assertThat(semantics.findSemanticsParent()).isEqualTo(rootSemantics)
- assertThat(semantics.findMergingSemanticsParent()).isNull()
- assertThat(semantics.findSemanticsChildren()).isEmpty()
- }
-
- @Test
- fun twoSemanticsModifiers() {
- // Arrange.
- rule.setTestContent {
- Box(Modifier.semantics { this.testTag = "item1" })
- Box(Modifier.semantics { this.testTag = "item2" })
- }
- rule.waitForIdle()
-
- // Act.
- val rootSemantics: SemanticsInfo = semanticsOwner.rootInfo
- val semantics1 = rule.getSemanticsInfoForTag("item1")
- val semantics2 = rule.getSemanticsInfoForTag("item2")
-
- // Assert.
- assertThat(rootSemantics.parentInfo).isNull()
- assertThat(rootSemantics.childrenInfo.map { it.semanticsConfiguration }.toList())
- .comparingElementsUsing(SemanticsConfigurationComparator)
- .containsExactly(
- SemanticsConfiguration().apply { testTag = "item1" },
- SemanticsConfiguration().apply { testTag = "item2" }
- )
- .inOrder()
-
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
- .comparingElementsUsing(SemanticsConfigurationComparator)
- .containsExactly(
- SemanticsConfiguration().apply { testTag = "item1" },
- SemanticsConfiguration().apply { testTag = "item2" }
- )
- .inOrder()
-
- checkNotNull(semantics1)
- assertThat(semantics1.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics1.childrenInfo.size).isEqualTo(0)
-
- checkNotNull(semantics2)
- assertThat(semantics2.parentInfo).isEqualTo(rootSemantics)
- assertThat(semantics2.childrenInfo.size).isEqualTo(0)
-
- // Assert extension Functions.
- assertThat(rootSemantics.findSemanticsParent()).isNull()
- assertThat(rootSemantics.findMergingSemanticsParent()).isNull()
- assertThat(rootSemantics.findSemanticsChildren().map { it.semanticsConfiguration })
- .comparingElementsUsing(SemanticsConfigurationComparator)
- .containsExactly(
- SemanticsConfiguration().apply { testTag = "item1" },
- SemanticsConfiguration().apply { testTag = "item2" }
- )
- .inOrder()
-
- assertThat(semantics1.findSemanticsParent()).isEqualTo(rootSemantics)
- assertThat(semantics1.findMergingSemanticsParent()).isNull()
- assertThat(semantics1.findSemanticsChildren()).isEmpty()
-
- assertThat(semantics2.findSemanticsParent()).isEqualTo(rootSemantics)
- assertThat(semantics2.findMergingSemanticsParent()).isNull()
- assertThat(semantics2.findSemanticsChildren()).isEmpty()
- }
-
- // TODO(ralu): Split this into multiple tests.
- @Test
- fun nodeDeepInHierarchy() {
- // Arrange.
- rule.setTestContent {
- Column(Modifier.semantics(mergeDescendants = true) { testTag = "outerColumn" }) {
- Row(Modifier.semantics { testTag = "outerRow" }) {
- Column(Modifier.semantics(mergeDescendants = true) { testTag = "column" }) {
- Row(Modifier.semantics { testTag = "row" }) {
- Column {
- Box(Modifier.semantics { testTag = "box" })
- Row(
- Modifier.semantics {}
- .semantics { testTag = "testTarget" }
- .semantics { testTag = "extra modifier2" }
- ) {
- Box { Box(Modifier.semantics { testTag = "child1" }) }
- Box(Modifier.semantics { testTag = "child2" }) {
- Box(Modifier.semantics { testTag = "grandChild" })
- }
- Box {}
- Row {
- Box {}
- Box {}
- }
- Box { Box(Modifier.semantics { testTag = "child3" }) }
- }
- }
- }
- }
- }
- }
- }
- rule.waitForIdle()
- val row = rule.getSemanticsInfoForTag(tag = "row", useUnmergedTree = true)
- val column = rule.getSemanticsInfoForTag("column")
-
- // Act.
- val testTarget = rule.getSemanticsInfoForTag(tag = "testTarget", useUnmergedTree = true)
-
- // Assert.
- checkNotNull(testTarget)
- assertThat(testTarget.parentInfo).isNotEqualTo(row)
- assertThat(testTarget.findSemanticsParent()).isEqualTo(row)
- assertThat(testTarget.findMergingSemanticsParent()).isEqualTo(column)
- assertThat(testTarget.childrenInfo.size).isEqualTo(5)
- assertThat(testTarget.findSemanticsChildren().map { it.semanticsConfiguration })
- .comparingElementsUsing(SemanticsConfigurationComparator)
- .containsExactly(
- SemanticsConfiguration().apply { testTag = "child1" },
- SemanticsConfiguration().apply { testTag = "child2" },
- SemanticsConfiguration().apply { testTag = "child3" }
- )
- .inOrder()
- assertThat(testTarget.semanticsConfiguration?.getOrNull(TestTag)).isEqualTo("testTarget")
- }
-
- private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
- setContent {
- semanticsOwner = (LocalView.current as RootForTest).semanticsOwner
- composable()
- }
- }
-
- /** Helper function that returns a list of children that is easier to assert on in tests. */
- private fun SemanticsInfo.findSemanticsChildren(): List<SemanticsInfo> {
- val children = mutableListOf<SemanticsInfo>()
- this@findSemanticsChildren.findSemanticsChildren { children.add(it) }
- return children
- }
-
- private fun ComposeContentTestRule.getSemanticsInfoForTag(
- tag: String,
- useUnmergedTree: Boolean = true
- ): SemanticsInfo? {
- return semanticsOwner[onNodeWithTag(tag, useUnmergedTree).semanticsId()]
- }
-
- companion object {
- private val SemanticsConfigurationComparator =
- Correspondence.from<SemanticsConfiguration, SemanticsConfiguration>(
- { actual, expected ->
- actual != null &&
- expected != null &&
- actual.getOrNull(TestTag) == expected.getOrNull(TestTag)
- },
- "has same test tag as "
- )
- }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt
deleted file mode 100644
index 77965fe..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/semantics/SemanticsListenerTest.kt
+++ /dev/null
@@ -1,556 +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.compose.ui.semantics
-
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.Text
-import androidx.compose.material.TextField
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.isExactly
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color.Companion.Black
-import androidx.compose.ui.graphics.Color.Companion.Red
-import androidx.compose.ui.node.RootForTest
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastJoinToString
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import org.junit.Before
-import org.junit.Rule
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class SemanticsListenerTest(private val isSemanticAutofillEnabled: Boolean) {
-
- @get:Rule val rule = createComposeRule()
-
- private lateinit var semanticsOwner: SemanticsOwner
-
- companion object {
- @JvmStatic
- @Parameterized.Parameters(name = "isSemanticAutofillEnabled = {0}")
- fun initParameters() = listOf(false, true)
- }
-
- @Before
- fun setup() {
- @OptIn(ExperimentalComposeUiApi::class)
- ComposeUiFlags.isSemanticAutofillEnabled = isSemanticAutofillEnabled
- }
-
- // Initial layout does not trigger listeners. Users have to detect the initial semantics
- // values by detecting first layout (You can get the bounds from RectManager.RectList).
- @Test
- fun initialComposition_doesNotTriggerListeners() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Text(text = "text")
- }
-
- // Assert.
- rule.runOnIdle { assertThat(events).isEmpty() }
- }
-
- @Test
- fun addingNonSemanticsModifier() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var addModifier by mutableStateOf(false)
- val text = AnnotatedString("text")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.then(if (addModifier) Modifier.size(1000.dp) else Modifier)
- .semantics { this.text = text }
- .testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { addModifier = true }
-
- // Assert.
- rule.runOnIdle { assertThat(events).isEmpty() }
- }
-
- @Test
- fun removingNonSemanticsModifier() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var removeModifier by mutableStateOf(false)
- val text = AnnotatedString("text")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.then(if (removeModifier) Modifier else Modifier.size(1000.dp))
- .semantics { this.text = text }
- .testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { removeModifier = true }
-
- // Assert.
- rule.runOnIdle { assertThat(events).isEmpty() }
- }
-
- @Test
- fun addingSemanticsModifier() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var addModifier by mutableStateOf(false)
- val text = AnnotatedString("text")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.size(100.dp)
- .then(
- if (addModifier) Modifier.semantics { this.text = text } else Modifier
- )
- .testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { addModifier = true }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = null, newSemantics = "text"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun removingSemanticsModifier() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var removeModifier by mutableStateOf(false)
- val text = AnnotatedString("text")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.size(1000.dp)
- .then(
- if (removeModifier) Modifier
- else Modifier.semantics { this.text = text }
- )
- .testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { removeModifier = true }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text", newSemantics = null))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun changingMutableSemanticsProperty() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var text by mutableStateOf(AnnotatedString("text1"))
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(modifier = Modifier.semantics { this.text = text }.testTag("item"))
- }
-
- // Act.
- rule.runOnIdle { text = AnnotatedString("text2") }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun changingMutableSemanticsProperty_alongWithRecomposition() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var text by mutableStateOf(AnnotatedString("text1"))
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.border(2.dp, if (text.text == "text1") Red else Black)
- .semantics { this.text = text }
- .testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { text = AnnotatedString("text2") }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun changingSemanticsProperty_andCallingInvalidateSemantics() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- val modifierNode =
- object : SemanticsModifierNode, Modifier.Node() {
- override fun SemanticsPropertyReceiver.applySemantics() {}
- }
- var text = AnnotatedString("text1")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Box(
- modifier =
- Modifier.elementFor(modifierNode).semantics { this.text = text }.testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle {
- text = AnnotatedString("text2")
- modifierNode.invalidateSemantics()
- }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun textChange() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var text by mutableStateOf("text1")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Text(text = text, modifier = Modifier.testTag("item"))
- }
-
- // Act.
- rule.runOnIdle { text = "text2" }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun multipleTextChanges() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var text by mutableStateOf("text1")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(Event(info.semanticsId, prev?.Text, info.semanticsConfiguration?.Text))
- }
- ) {
- Text(text = text, modifier = Modifier.testTag("item"))
- }
-
- // Act.
- rule.runOnIdle { text = "text2" }
- rule.runOnIdle { text = "text3" }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(
- Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"),
- Event(semanticsId, prevSemantics = "text2", newSemantics = "text3")
- )
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun EditTextChange() {
- // Arrange.
- val events = mutableListOf<Event<String>>()
- var text by mutableStateOf("text1")
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(
- Event(
- info.semanticsId,
- prev?.EditableText,
- info.semanticsConfiguration?.EditableText
- )
- )
- }
- ) {
- TextField(
- value = text,
- onValueChange = { text = it },
- modifier = Modifier.testTag("item")
- )
- }
-
- // Act.
- rule.runOnIdle { text = "text2" }
-
- // Assert.
- val semanticsId = rule.onNodeWithTag("item").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(Event(semanticsId, prevSemantics = "text1", newSemantics = "text2"))
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun FocusChange_withNoRecomposition() {
- // Arrange.
- val events = mutableListOf<Event<Boolean>>()
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(
- Event(
- info.semanticsId,
- prev?.getOrNull(SemanticsProperties.Focused),
- info.semanticsConfiguration?.getOrNull(SemanticsProperties.Focused)
- )
- )
- }
- ) {
- Column {
- Box(Modifier.testTag("item1").size(100.dp).focusable())
- Box(Modifier.testTag("item2").size(100.dp).focusable())
- }
- }
- rule.onNodeWithTag("item1").requestFocus()
- rule.runOnIdle { events.clear() }
-
- // Act.
- rule.onNodeWithTag("item2").requestFocus()
-
- // Assert.
- val item1 = rule.onNodeWithTag("item1").semanticsId
- val item2 = rule.onNodeWithTag("item2").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(
- Event(item1, prevSemantics = true, newSemantics = false),
- Event(item2, prevSemantics = false, newSemantics = true)
- )
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- @Test
- fun FocusChange_thatCausesRecomposition() {
- // Arrange.
- val events = mutableListOf<Event<Boolean>>()
- rule.setTestContent(
- onSemanticsChange = { info, prev ->
- events.add(
- Event(
- info.semanticsId,
- prev?.getOrNull(SemanticsProperties.Focused),
- info.semanticsConfiguration?.getOrNull(SemanticsProperties.Focused)
- )
- )
- }
- ) {
- Column {
- FocusableBox(Modifier.testTag("item1"))
- FocusableBox(Modifier.testTag("item2"))
- }
- }
- rule.onNodeWithTag("item1").requestFocus()
- rule.runOnIdle { events.clear() }
-
- // Act.
- rule.onNodeWithTag("item2").requestFocus()
-
- // Assert.
- val item1 = rule.onNodeWithTag("item1").semanticsId
- val item2 = rule.onNodeWithTag("item2").semanticsId
- rule.runOnIdle {
- if (isSemanticAutofillEnabled) {
- assertThat(events)
- .isExactly(
- Event(item1, prevSemantics = true, newSemantics = false),
- Event(item2, prevSemantics = false, newSemantics = true)
- )
- } else {
- assertThat(events).isEmpty()
- }
- }
- }
-
- private val SemanticsConfiguration.Text
- get() = getOrNull(SemanticsProperties.Text)?.fastJoinToString()
-
- private val SemanticsConfiguration.EditableText
- get() = getOrNull(SemanticsProperties.EditableText)?.toString()
-
- private fun ComposeContentTestRule.setTestContent(
- onSemanticsChange: (SemanticsInfo, SemanticsConfiguration?) -> Unit,
- composable: @Composable () -> Unit
- ) {
- val semanticsListener =
- object : SemanticsListener {
- override fun onSemanticsChanged(
- semanticsInfo: SemanticsInfo,
- previousSemanticsConfiguration: SemanticsConfiguration?
- ) {
- onSemanticsChange(semanticsInfo, previousSemanticsConfiguration)
- }
- }
- setContent {
- semanticsOwner = (LocalView.current as RootForTest).semanticsOwner
- DisposableEffect(semanticsOwner) {
- semanticsOwner.listeners.add(semanticsListener)
- onDispose { semanticsOwner.listeners.remove(semanticsListener) }
- }
- composable()
- }
- }
-
- data class Event<T>(val semanticsId: Int, val prevSemantics: T?, val newSemantics: T?)
-
- // TODO(b/272068594): Add api to fetch the semantics id from SemanticsNodeInteraction directly.
- private val SemanticsNodeInteraction.semanticsId: Int
- get() = fetchSemanticsNode().id
-
- @Composable
- private fun FocusableBox(
- modifier: Modifier = Modifier,
- content: @Composable BoxScope.() -> Unit = {}
- ) {
- var borderColor by remember { mutableStateOf(Black) }
- Box(
- modifier =
- modifier
- .size(100.dp)
- .onFocusChanged { borderColor = if (it.isFocused) Red else Black }
- .border(2.dp, borderColor)
- .focusable(),
- content = content
- )
- }
-}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index b86440d..ff82cbf 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -298,8 +298,6 @@
SemanticsContentDataType
)
}
- // TODO(b/138549623): Instead of creating a flattened tree by using the nodes from the map, we
- // can use SemanticsOwner to get the root SemanticsInfo and create a more representative tree.
var index = AutofillApi26Helper.addChildCount(root, count)
// Iterate through currentSemanticsNodes, finding autofill-related nodes
@@ -479,7 +477,7 @@
}
@RequiresApi(Build.VERSION_CODES.O)
-private class AutofillManagerWrapperImpl(val view: View) : AutofillManagerWrapper {
+private class AutofillManagerWrapperImpl(val view: AndroidComposeView) : AutofillManagerWrapper {
override val autofillManager =
view.context.getSystemService(PlatformAndroidManager::class.java)
?: error("Autofill service could not be located.")
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 7dc9d64..dfac967 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -63,8 +63,6 @@
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
-import androidx.collection.MutableIntObjectMap
-import androidx.collection.mutableIntObjectMapOf
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -431,12 +429,9 @@
.then(dragAndDropManager.modifier)
}
- override val layoutNodes: MutableIntObjectMap<LayoutNode> = mutableIntObjectMapOf()
-
override val rootForTest: RootForTest = this
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, rootSemanticsNode, layoutNodes)
+ override val semanticsOwner: SemanticsOwner = SemanticsOwner(root, rootSemanticsNode)
private val composeAccessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this)
internal var contentCaptureManager =
AndroidContentCaptureManager(
@@ -1031,12 +1026,9 @@
composeAccessibilityDelegate.SendRecurringAccessibilityEventsIntervalMillis = intervalMillis
}
- override fun onAttach(node: LayoutNode) {
- layoutNodes[node.semanticsId] = node
- }
+ override fun onAttach(node: LayoutNode) {}
override fun onDetach(node: LayoutNode) {
- layoutNodes.remove(node.semanticsId)
measureAndLayoutDelegate.onNodeDetached(node)
requestClearInvalidObservations()
@OptIn(ExperimentalComposeUiApi::class)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 7deeca8..7036817 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -2355,11 +2355,11 @@
if (layoutNode.nodes.has(Nodes.Semantics)) layoutNode
else layoutNode.findClosestParentNode { it.nodes.has(Nodes.Semantics) }
- val config = semanticsNode?.semanticsConfiguration ?: return
+ val config = semanticsNode?.collapsedSemantics ?: return
if (!config.isMergingSemanticsOfDescendants) {
semanticsNode
.findClosestParentNode {
- it.semanticsConfiguration?.isMergingSemanticsOfDescendants == true
+ it.collapsedSemantics?.isMergingSemanticsOfDescendants == true
}
?.let { semanticsNode = it }
}
@@ -3264,12 +3264,12 @@
val ancestor =
layoutNode.findClosestParentNode {
// looking for text field merging node
- val ancestorSemanticsConfiguration = it.semanticsConfiguration
+ val ancestorSemanticsConfiguration = it.collapsedSemantics
ancestorSemanticsConfiguration?.isMergingSemanticsOfDescendants == true &&
ancestorSemanticsConfiguration.contains(SemanticsProperties.EditableText)
}
return ancestor != null &&
- ancestor.semanticsConfiguration?.getOrNull(SemanticsProperties.Focused) != true
+ ancestor.collapsedSemantics?.getOrNull(SemanticsProperties.Focused) != true
}
private fun AccessibilityAction<*>.accessibilityEquals(other: Any?): Boolean {
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index c884b74..1f96189 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -17,8 +17,6 @@
package androidx.compose.ui.node
-import androidx.collection.IntObjectMap
-import androidx.collection.intObjectMapOf
import androidx.compose.testutils.TestViewConfiguration
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
@@ -68,10 +66,8 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
import androidx.compose.ui.platform.invertTo
-import androidx.compose.ui.semantics.EmptySemanticsModifier
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsModifier
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
@@ -2318,8 +2314,6 @@
internal class MockOwner(
private val position: IntOffset = IntOffset.Zero,
override val root: LayoutNode = LayoutNode(),
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, EmptySemanticsModifier(), intObjectMapOf()),
override val coroutineContext: CoroutineContext =
Executors.newFixedThreadPool(3).asCoroutineDispatcher()
) : Owner {
@@ -2553,9 +2547,6 @@
override val viewConfiguration: ViewConfiguration
get() = TODO("Not yet implemented")
- override val layoutNodes: IntObjectMap<LayoutNode>
- get() = TODO("Not yet implemented")
-
override val sharedDrawScope = LayoutNodeDrawScope()
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index bc17a55..8c77a83 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -18,8 +18,6 @@
package androidx.compose.ui.node
-import androidx.collection.IntObjectMap
-import androidx.collection.intObjectMapOf
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -52,8 +50,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.EmptySemanticsModifier
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -337,9 +333,7 @@
override fun onDetach(node: LayoutNode) {}
- override val root: LayoutNode = LayoutNode()
-
- override val layoutNodes: IntObjectMap<LayoutNode>
+ override val root: LayoutNode
get() = TODO("Not yet implemented")
override val sharedDrawScope: LayoutNodeDrawScope
@@ -381,9 +375,6 @@
override val focusOwner: FocusOwner
get() = TODO("Not yet implemented")
- override val semanticsOwner: SemanticsOwner =
- SemanticsOwner(root, EmptySemanticsModifier(), intObjectMapOf())
-
override val windowInfo: WindowInfo
get() = TODO("Not yet implemented")
@@ -451,7 +442,7 @@
override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) =
TODO("Not yet implemented")
- override fun onSemanticsChange() {}
+ override fun onSemanticsChange() = TODO("Not yet implemented")
override fun onLayoutChange(layoutNode: LayoutNode) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index ace8eed..2da7de0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -19,8 +19,6 @@
import androidx.compose.runtime.CompositionLocalMap
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.ComposeUiFlags
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@@ -56,7 +54,6 @@
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.SemanticsConfiguration
-import androidx.compose.ui.semantics.SemanticsInfo
import androidx.compose.ui.semantics.generateSemanticsId
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -91,7 +88,6 @@
Remeasurement,
OwnerScope,
LayoutInfo,
- SemanticsInfo,
ComposeUiNode,
InteroperableComposeUiNode,
Owner.OnLayoutCompletedListener {
@@ -398,62 +394,43 @@
invalidateMeasurements()
}
- private var _semanticsConfiguration: SemanticsConfiguration? = null
- override val semanticsConfiguration: SemanticsConfiguration?
- get() {
- // This is needed until we completely move to the new world where we always pre-compute
- // the semantics configuration. At that point, this can be replaced by
- // check(!isSemanticsInvalidated) or remove this custom getter.
- if (isSemanticsInvalidated) {
- _semanticsConfiguration = calculateSemanticsConfiguration()
- }
- return _semanticsConfiguration
- }
-
- private fun calculateSemanticsConfiguration(): SemanticsConfiguration? {
- if (!nodes.has(Nodes.Semantics)) return null
-
- var config = SemanticsConfiguration()
- requireOwner().snapshotObserver.observeSemanticsReads(this) {
- nodes.tailToHead(Nodes.Semantics) {
- if (it.shouldClearDescendantSemantics) {
- config = SemanticsConfiguration()
- config.isClearingSemantics = true
- }
- if (it.shouldMergeDescendantSemantics) {
- config.isMergingSemanticsOfDescendants = true
- }
- with(config) { with(it) { applySemantics() } }
- }
- }
- return config
- }
-
- private var isSemanticsInvalidated = false
+ private var _collapsedSemantics: SemanticsConfiguration? = null
internal fun invalidateSemantics() {
- if (
- @OptIn(ExperimentalComposeUiApi::class) !ComposeUiFlags.isSemanticAutofillEnabled ||
- nodes.isUpdating ||
- applyingModifierOnAttach
- ) {
- // We are currently updating the modifier, so just schedule an invalidation. After
- // applying the modifier, we will notify listeners of semantics changes.
- isSemanticsInvalidated = true
- } else {
- // We are not currently updating the modifier, so instead of scheduling invalidation,
- // we update the semantics configuration and send the notification event right away.
- val prev = _semanticsConfiguration
- _semanticsConfiguration = calculateSemanticsConfiguration()
- requireOwner().semanticsOwner.notifySemanticsChange(this, prev)
- }
-
+ _collapsedSemantics = null
// TODO(lmr): this ends up scheduling work that diffs the entire tree, but we should
// eventually move to marking just this node as invalidated since we are invalidating
// on a per-node level. This should preserve current behavior for now.
requireOwner().onSemanticsChange()
}
+ internal val collapsedSemantics: SemanticsConfiguration?
+ get() {
+ // TODO: investigate if there's a better way to approach "half attached" state and
+ // whether or not deactivated nodes should be considered removed or not.
+ if (!isAttached || isDeactivated) return null
+
+ if (!nodes.has(Nodes.Semantics) || _collapsedSemantics != null) {
+ return _collapsedSemantics
+ }
+
+ var config = SemanticsConfiguration()
+ requireOwner().snapshotObserver.observeSemanticsReads(this) {
+ nodes.tailToHead(Nodes.Semantics) {
+ if (it.shouldClearDescendantSemantics) {
+ config = SemanticsConfiguration()
+ config.isClearingSemantics = true
+ }
+ if (it.shouldMergeDescendantSemantics) {
+ config.isMergingSemanticsOfDescendants = true
+ }
+ with(config) { with(it) { applySemantics() } }
+ }
+ }
+ _collapsedSemantics = config
+ return config
+ }
+
/**
* Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached. [owner]
* must match its [parent].[owner].
@@ -486,7 +463,9 @@
pendingModifier?.let { applyModifier(it) }
pendingModifier = null
- if (nodes.has(Nodes.Semantics)) invalidateSemantics()
+ if (nodes.has(Nodes.Semantics)) {
+ invalidateSemantics()
+ }
owner.onAttach(this)
// Update lookahead root when attached. For nested cases, we'll always use the
@@ -538,9 +517,9 @@
}
layoutDelegate.resetAlignmentLines()
onDetach?.invoke(owner)
+
if (nodes.has(Nodes.Semantics)) {
- _semanticsConfiguration = null
- requireOwner().onSemanticsChange()
+ invalidateSemantics()
}
nodes.runDetachLifecycle()
ignoreRemeasureRequests { _foldedChildren.forEach { child -> child.detach() } }
@@ -576,10 +555,6 @@
return _zSortedChildren
}
- @Suppress("UNCHECKED_CAST")
- override val childrenInfo: MutableVector<SemanticsInfo>
- get() = zSortedChildren as MutableVector<SemanticsInfo>
-
override val isValidOwnerScope: Boolean
get() = isAttached
@@ -891,14 +866,6 @@
if (lookaheadRoot == null && nodes.has(Nodes.ApproachMeasure)) {
lookaheadRoot = this
}
- // Notify semantics listeners if semantics was invalidated.
- @OptIn(ExperimentalComposeUiApi::class)
- if (ComposeUiFlags.isSemanticAutofillEnabled && isSemanticsInvalidated) {
- val prev = _semanticsConfiguration
- _semanticsConfiguration = calculateSemanticsConfiguration()
- isSemanticsInvalidated = false
- requireOwner().semanticsOwner.notifySemanticsChange(this, prev)
- }
}
private fun resetModifierState() {
@@ -1303,7 +1270,7 @@
}
}
- override val parentInfo: SemanticsInfo?
+ override val parentInfo: LayoutInfo?
get() = parent
override var isDeactivated = false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index c08a929..ff19036 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -40,8 +40,8 @@
internal var head: Modifier.Node = tail
private set
- internal val isUpdating: Boolean
- get() = head.parent != null
+ private val isUpdating: Boolean
+ get() = head === SentinelHead
private val aggregateChildKindSet: Int
get() = head.aggregateChildKindSet
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index f102b02..0ce80dd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1512,7 +1512,7 @@
override fun interceptOutOfBoundsChildEvents(node: Modifier.Node) = false
override fun shouldHitTestChildren(parentLayoutNode: LayoutNode) =
- parentLayoutNode.semanticsConfiguration?.isClearingSemantics != true
+ parentLayoutNode.collapsedSemantics?.isClearingSemantics != true
override fun childHitTest(
layoutNode: LayoutNode,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 655a320..ed5b7c2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -18,7 +18,6 @@
package androidx.compose.ui.node
import androidx.annotation.RestrictTo
-import androidx.collection.IntObjectMap
import androidx.compose.runtime.Applier
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
@@ -48,7 +47,6 @@
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.WindowInfo
-import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.spatial.RectManager
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@@ -70,9 +68,6 @@
/** The root layout node in the component tree. */
val root: LayoutNode
- /** A mapping of semantic id to LayoutNode. */
- val layoutNodes: IntObjectMap<LayoutNode>
-
/** Draw scope reused for drawing speed up. */
val sharedDrawScope: LayoutNodeDrawScope
@@ -134,8 +129,6 @@
val pointerIconService: PointerIconService
- val semanticsOwner: SemanticsOwner
-
/** Provide a focus owner that controls focus within Compose. */
val focusOwner: FocusOwner
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
index 002be43..dd3e2f8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
@@ -87,14 +87,7 @@
fun SemanticsPropertyReceiver.applySemantics()
}
-/**
- * Invalidate semantics associated with this node. This will reset the [SemanticsConfiguration]
- * associated with the layout node backing this modifier node, and will re-calculate it the next
- * time the [SemanticsConfiguration] is read.
- */
-fun SemanticsModifierNode.invalidateSemantics() {
- requireLayoutNode().invalidateSemantics()
-}
+fun SemanticsModifierNode.invalidateSemantics() = requireLayoutNode().invalidateSemantics()
internal val SemanticsConfiguration.useMinimumTouchTarget: Boolean
get() = getOrNull(SemanticsActions.OnClick) != null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
deleted file mode 100644
index 1f2cc5d..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsInfo.kt
+++ /dev/null
@@ -1,85 +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.compose.ui.semantics
-
-import androidx.compose.runtime.collection.MutableVector
-import androidx.compose.ui.layout.LayoutInfo
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.node.LayoutNode
-
-/**
- * This is an internal interface that can be used by [SemanticsListener]s to read semantic
- * information from layout nodes. The root [SemanticsInfo] can be accessed using
- * [SemanticsOwner.rootInfo], and particular [SemanticsInfo] can be looked up by their [semanticsId]
- * by using [SemanticsOwner.get].
- */
-internal interface SemanticsInfo : LayoutInfo {
- /** The semantics configuration (Semantic properties and actions) associated with this node. */
- val semanticsConfiguration: SemanticsConfiguration?
-
- /**
- * The [SemanticsInfo] of the parent.
- *
- * This includes parents that do not have any semantics modifiers.
- */
- override val parentInfo: SemanticsInfo?
-
- /**
- * Returns the children list sorted by their [LayoutNode.zIndex] first (smaller first) and the
- * order they were placed via [Placeable.placeAt] by parent (smaller first). Please note that
- * this list contains not placed items as well, so you have to manually filter them.
- *
- * Note that the object is reused so you shouldn't save it for later.
- */
- val childrenInfo: MutableVector<SemanticsInfo>
-}
-
-/** The semantics parent (nearest ancestor which has semantic properties). */
-internal fun SemanticsInfo.findSemanticsParent(): SemanticsInfo? {
- var parent = parentInfo
- while (parent != null) {
- if (parent.semanticsConfiguration != null) return parent
- parent = parent.parentInfo
- }
- return null
-}
-
-/** The nearest semantics ancestor that is merging descendants. */
-internal fun SemanticsInfo.findMergingSemanticsParent(): SemanticsInfo? {
- var parent = parentInfo
- while (parent != null) {
- if (parent.semanticsConfiguration?.isMergingSemanticsOfDescendants == true) return parent
- parent = parent.parentInfo
- }
- return null
-}
-
-internal inline fun SemanticsInfo.findSemanticsChildren(
- includeDeactivated: Boolean = false,
- block: (SemanticsInfo) -> Unit
-) {
- val unvisitedStack = MutableVector<SemanticsInfo>(childrenInfo.size)
- childrenInfo.forEachReversed { unvisitedStack += it }
- while (unvisitedStack.isNotEmpty()) {
- val child = unvisitedStack.removeAt(unvisitedStack.lastIndex)
- when {
- child.isDeactivated && !includeDeactivated -> continue
- child.semanticsConfiguration != null -> block(child)
- else -> child.childrenInfo.forEachReversed { unvisitedStack += it }
- }
- }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsListener.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsListener.kt
deleted file mode 100644
index b51d7c8..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsListener.kt
+++ /dev/null
@@ -1,34 +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.compose.ui.semantics
-
-/** A listener that can be used to observe semantic changes. */
-internal interface SemanticsListener {
-
- /**
- * [onSemanticsChanged] is called when the [SemanticsConfiguration] of a LayoutNode changes, or
- * when a node calls SemanticsModifierNode.invalidateSemantics.
- *
- * @param semanticsInfo the current [SemanticsInfo] of the layout node that has changed.
- * @param previousSemanticsConfiguration the previous [SemanticsConfiguration] associated with
- * the layout node.
- */
- fun onSemanticsChanged(
- semanticsInfo: SemanticsInfo,
- previousSemanticsConfiguration: SemanticsConfiguration?
- )
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 4bbacd0..35478d6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -45,7 +45,7 @@
layoutNode.nodes.head(Nodes.Semantics)!!.node,
mergingEnabled,
layoutNode,
- layoutNode.semanticsConfiguration!!
+ layoutNode.collapsedSemantics!!
)
internal fun SemanticsNode(
@@ -70,7 +70,7 @@
outerSemanticsNode.node,
mergingEnabled,
layoutNode,
- layoutNode.semanticsConfiguration ?: SemanticsConfiguration()
+ layoutNode.collapsedSemantics ?: SemanticsConfiguration()
)
/**
@@ -99,7 +99,7 @@
!isFake &&
replacedChildren.isEmpty() &&
layoutNode.findClosestParentNode {
- it.semanticsConfiguration?.isMergingSemanticsOfDescendants == true
+ it.collapsedSemantics?.isMergingSemanticsOfDescendants == true
} == null
/** The [LayoutInfo] that this is associated with. */
@@ -345,7 +345,7 @@
if (mergingEnabled) {
node =
this.layoutNode.findClosestParentNode {
- it.semanticsConfiguration?.isMergingSemanticsOfDescendants == true
+ it.collapsedSemantics?.isMergingSemanticsOfDescendants == true
}
}
@@ -474,9 +474,7 @@
* Executes [selector] on every parent of this [LayoutNode] and returns the closest [LayoutNode] to
* return `true` from [selector] or null if [selector] returns false for all ancestors.
*/
-internal inline fun LayoutNode.findClosestParentNode(
- selector: (LayoutNode) -> Boolean
-): LayoutNode? {
+internal fun LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
var currentParent = this.parent
while (currentParent != null) {
if (selector(currentParent)) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
index b987155..dffed0f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
@@ -16,8 +16,6 @@
package androidx.compose.ui.semantics
-import androidx.collection.IntObjectMap
-import androidx.collection.MutableObjectList
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.util.fastForEach
@@ -25,8 +23,7 @@
class SemanticsOwner
internal constructor(
private val rootNode: LayoutNode,
- private val outerSemanticsNode: EmptySemanticsModifier,
- private val nodes: IntObjectMap<LayoutNode>
+ private val outerSemanticsNode: EmptySemanticsModifier
) {
/**
* The root node of the semantics tree. Does not contain any unmerged data. May contain merged
@@ -50,22 +47,6 @@
unmergedConfig = SemanticsConfiguration()
)
}
-
- internal val listeners = MutableObjectList<SemanticsListener>(2)
-
- internal val rootInfo: SemanticsInfo
- get() = rootNode
-
- internal operator fun get(semanticsId: Int): SemanticsInfo? {
- return nodes[semanticsId]
- }
-
- internal fun notifySemanticsChange(
- semanticsInfo: SemanticsInfo,
- previousSemanticsConfiguration: SemanticsConfiguration?
- ) {
- listeners.forEach { it.onSemanticsChanged(semanticsInfo, previousSemanticsConfiguration) }
- }
}
/**
@@ -89,7 +70,6 @@
.toList()
}
-@Suppress("unused")
@Deprecated(message = "Use a new overload instead", level = DeprecationLevel.HIDDEN)
fun SemanticsOwner.getAllSemanticsNodes(mergingEnabled: Boolean) =
getAllSemanticsNodes(mergingEnabled, true)