Merge "BTF2 filtered text change should remove composition" into androidx-main
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 21afe36..44cc5f7 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -19,8 +19,8 @@
 import androidx.compose.foundation.demos.text2.BasicSecureTextFieldDemos
 import androidx.compose.foundation.demos.text2.BasicTextFieldCustomPinFieldDemo
 import androidx.compose.foundation.demos.text2.BasicTextFieldDemos
-import androidx.compose.foundation.demos.text2.BasicTextFieldFilterDemos
 import androidx.compose.foundation.demos.text2.BasicTextFieldInScrollableDemo
+import androidx.compose.foundation.demos.text2.BasicTextFieldInputTransformationDemos
 import androidx.compose.foundation.demos.text2.BasicTextFieldLongTextDemo
 import androidx.compose.foundation.demos.text2.BasicTextFieldOutputTransformationDemos
 import androidx.compose.foundation.demos.text2.BasicTextFieldValueCallbackDemo
@@ -169,12 +169,14 @@
                     ComposableDemo("Rtl") { ScrollableDemosRtl() },
                 )),
                 ComposableDemo("Inside Scrollable") { BasicTextFieldInScrollableDemo() },
-                ComposableDemo("Filters") { BasicTextFieldFilterDemos() },
+                ComposableDemo("Input Transformation") {
+                    BasicTextFieldInputTransformationDemos()
+                },
                 DemoCategory("Receive Content", listOf(
                     ComposableDemo("Basic") { TextFieldReceiveContentDemo() },
                     ComposableDemo("Nested") { NestedReceiveContentDemo() },
                 )),
-                ComposableDemo("Output transformation") {
+                ComposableDemo("Output Transformation") {
                     BasicTextFieldOutputTransformationDemos()
                 },
                 ComposableDemo("Secure Field") { BasicSecureTextFieldDemos() },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
similarity index 90%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
index efdff20..1b5b733 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
@@ -37,6 +37,7 @@
 import androidx.compose.foundation.text.input.TextFieldBuffer
 import androidx.compose.foundation.text.input.TextFieldState
 import androidx.compose.foundation.text.input.allCaps
+import androidx.compose.foundation.text.input.forEachChange
 import androidx.compose.foundation.text.input.maxLength
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Switch
@@ -52,7 +53,7 @@
 import androidx.core.text.isDigitsOnly
 
 @Composable
-fun BasicTextFieldFilterDemos() {
+fun BasicTextFieldInputTransformationDemos() {
     Column(
         Modifier
             .imePadding()
@@ -67,6 +68,9 @@
         TagLine(tag = "Digits Only BasicTextField")
         DigitsOnlyDemo()
 
+        TagLine(tag = "Additive InputTransformation")
+        AdditiveInputTransformationDemo()
+
         TagLine(tag = "Change filter")
         ChangeFilterDemo()
 
@@ -97,7 +101,6 @@
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 private fun DigitsOnlyDemo() {
     FilterDemo(filter = object : InputTransformation {
@@ -114,6 +117,18 @@
 }
 
 @Composable
+private fun AdditiveInputTransformationDemo() {
+    FilterDemo(filter = {
+        changes.forEachChange { range, originalRange ->
+            // only extend the insertions
+            if (!range.collapsed && originalRange.collapsed) {
+                replace(range.end, range.end, "a")
+            }
+        }
+    })
+}
+
+@Composable
 private fun FilterDemo(filter: InputTransformation) {
     val state = remember { TextFieldState() }
     BasicTextField(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 6a23981..2717eb0 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -552,6 +552,25 @@
     }
 
     @Test
+    fun textField_appliesFilter_toInputConnection_changingComposition() {
+        val state = TextFieldState()
+        inputMethodInterceptor.setTextFieldTestContent {
+            BasicTextField(
+                state = state,
+                inputTransformation = RejectAllTextFilter,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+        requestFocus(Tag)
+
+        inputMethodInterceptor.withInputConnection {
+            setComposingText("hello", 1)
+        }
+        rule.onNodeWithTag(Tag).assertTextEquals("")
+        assertThat(state.composition).isNull()
+    }
+
+    @Test
     fun textField_appliesFilter_toSetTextSemanticsAction() {
         val state = TextFieldState()
         inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
index 2a4f9b2..6b053e2 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.text.input
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.input.internal.setComposingText
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.SnapshotStateObserver
@@ -699,6 +700,18 @@
         assertThat(transformationCalled).isEqualTo(0)
     }
 
+    @Test
+    fun inputTransformationRejectsChanges_removesComposition() {
+        val state = TextFieldState()
+        val inputTransformation = InputTransformation { revertAllChanges() }
+        state.editAsUser(inputTransformation) {
+            setComposingText("hello", 1)
+        }
+        assertThat(state.text).isEqualTo("")
+        assertThat(state.selection).isEqualTo(TextRange.Zero)
+        assertThat(state.composition).isNull()
+    }
+
     private fun runTestWithSnapshotsThenCancelChildren(testBody: suspend TestScope.() -> Unit) {
         val globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
             // This is normally done by the compose runtime.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index 20cfe61..49f0790 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -377,7 +377,9 @@
         if (textChangedByFilter || selectionChangedByFilter) {
             syncMainBufferToTemporaryBuffer(
                 textFieldBuffer = textFieldBuffer,
-                newComposition = afterEditValue.composition,
+                // Composition should be decided by the IME after the content or selection has been
+                // changed programmatically, outside the knowledge of IME.
+                newComposition = null,
                 textChanged = textChangedByFilter,
                 selectionChanged = selectionChangedByFilter
             )