Revert removing implementations of notifyFocusedRect.
The notifyFocusedRect methods on TextInputService and TextInputSession
were deprecated and had their implementations replaced by no-ops in the
change aosp/1959647. This was probably too aggressive, so this change
adds the old implementations back in but leaves them deprecated, in case
any third-party code was calling them. The implementations were actually
broken before, and this change doesn't try to fix them, it just restores
the previous broken behavior.
Discussion thread about revert:
https://android-review.googlesource.com/c/platform/frameworks/support/+/1959647/comments/81f97bed_7f38e815
Bug: b/192043120
Bug: b/178211874
Test: Old tests restored.
Change-Id: I385749b629fd35b72bd148122ab1ec0f6d4ffa96
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index a97d3cd..79cbf37 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -147,9 +147,11 @@
}
}
- @Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER")
- @Deprecated("This method is not called, used BringIntoViewRequester instead.")
- fun notifyFocusedRect(rect: Rect): Boolean = false
+ @Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION")
+ @Deprecated("This method should not be called, used BringIntoViewRequester instead.")
+ fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession {
+ platformTextInputService.notifyFocusedRect(rect)
+ }
/**
* Notify IME about the new [TextFieldValue] and latest state of the editing buffer. [oldValue]
@@ -253,7 +255,7 @@
*/
fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue)
- @Deprecated("This method is not called, used BringIntoViewRequester instead.")
+ @Deprecated("This method should not be called, used BringIntoViewRequester instead.")
fun notifyFocusedRect(rect: Rect) {
}
}
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
index c904ccd..10b25c6 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextInputServiceTest.kt
@@ -16,6 +16,9 @@
package androidx.compose.ui.text
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextFieldValue
@@ -31,6 +34,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+@Suppress("DEPRECATION")
@RunWith(JUnit4::class)
class TextInputServiceTest {
@@ -235,4 +239,51 @@
secondSession.updateState(null, editorModel)
verify(platformService).updateState(eq(null), eq(editorModel))
}
+
+ @Test
+ fun notifyFocusedRect_with_valid_token() {
+ val platformService = mock<PlatformTextInputService>()
+
+ val textInputService = TextInputService(platformService)
+
+ val firstSession = textInputService.startInput(
+ TextFieldValue(),
+ ImeOptions.Default,
+ {}, // onEditCommand
+ {} // onImeActionPerformed
+ )
+
+ val rect = Rect(Offset.Zero, Size(100f, 100f))
+ firstSession.notifyFocusedRect(rect)
+ verify(platformService, times(1)).notifyFocusedRect(eq(rect))
+ }
+
+ @Test
+ fun notifyFocusedRect_with_expired_token() {
+ val platformService = mock<PlatformTextInputService>()
+
+ val textInputService = TextInputService(platformService)
+
+ val firstSession = textInputService.startInput(
+ TextFieldValue(),
+ ImeOptions.Default,
+ {}, // onEditCommand
+ {} // onImeActionPerformed
+ )
+
+ // Start another session. The firstToken is now expired.
+ val secondSession = textInputService.startInput(
+ TextFieldValue(),
+ ImeOptions.Default,
+ {}, // onEditCommand
+ {} // onImeActionPerformed
+ )
+
+ val rect = Rect(Offset.Zero, Size(100f, 100f))
+ firstSession.notifyFocusedRect(rect)
+ verify(platformService, never()).notifyFocusedRect(any())
+
+ secondSession.notifyFocusedRect(rect)
+ verify(platformService, times(1)).notifyFocusedRect(eq(rect))
+ }
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 39afec1..d46d49d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -16,6 +16,7 @@
package androidx.compose.ui.text.input
+import android.graphics.Rect as AndroidRect
import android.text.InputType
import android.util.Log
import android.view.KeyEvent
@@ -23,12 +24,14 @@
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.HideKeyboard
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.ShowKeyboard
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StartInput
import androidx.compose.ui.text.input.TextInputServiceAndroid.TextInputCommand.StopInput
import androidx.core.view.inputmethod.EditorInfoCompat
+import kotlin.math.roundToInt
import kotlinx.coroutines.channels.Channel
private const val DEBUG_CLASS = "TextInputServiceAndroid"
@@ -70,11 +73,14 @@
private set
private var imeOptions = ImeOptions.Default
private var ic: RecordingInputConnection? = null
+
// used for sendKeyEvent delegation
private val baseInputConnection by lazy(LazyThreadSafetyMode.NONE) {
BaseInputConnection(view, false)
}
+ private var focusedRect: AndroidRect? = null
+
/**
* A channel that is used to debounce rapid operations such as showing/hiding the keyboard and
* starting/stopping input, so we can make the minimal number of calls on the
@@ -153,6 +159,7 @@
editorHasFocus = false
onEditCommand = {}
onImeActionPerformed = {}
+ focusedRect = null
// Don't actually send the command to the IME yet, it may be overruled by a subsequent call
// to startInput.
@@ -327,6 +334,30 @@
}
}
+ @Suppress("OverridingDeprecatedMember")
+ override fun notifyFocusedRect(rect: Rect) {
+ focusedRect = AndroidRect(
+ rect.left.roundToInt(),
+ rect.top.roundToInt(),
+ rect.right.roundToInt(),
+ rect.bottom.roundToInt()
+ )
+
+ // Requesting rectangle too early after obtaining focus may bring view into wrong place
+ // probably due to transient IME inset change. We don't know the correct timing of calling
+ // requestRectangleOnScreen API, so try to call this API only after the IME is ready to
+ // use, i.e. InputConnection has created.
+ // Even if we miss all the timing of requesting rectangle during initial text field focus,
+ // focused rectangle will be requested when software keyboard has shown.
+ if (ic == null) {
+ focusedRect?.let {
+ // Notice that view.requestRectangleOnScreen may modify the input Rect, we have to
+ // create another Rect and then pass it.
+ view.requestRectangleOnScreen(AndroidRect(it))
+ }
+ }
+ }
+
/** Immediately restart the IME connection, bypassing the [textInputCommandChannel]. */
private fun restartInputImmediately() {
if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")