TextInputServiceAndroid.kt
/*
* Copyright 2019 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.text.input
import android.content.Context
import android.text.InputType
import android.view.View
import android.view.ViewTreeObserver
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextRange
import kotlin.math.roundToInt
/**
* Provide Android specific input service with the Operating System.
*/
@OptIn(ExperimentalTextApi::class)
internal class TextInputServiceAndroid(val view: View) : PlatformTextInputService {
/** True if the currently editable composable has connected */
private var editorHasFocus = false
/**
* The following three observers are set when the editable composable has initiated the input
* session
*/
private var onEditCommand: (List<EditOperation>) -> Unit = {}
private var onImeActionPerformed: (ImeAction) -> Unit = {}
private var state = TextFieldValue(text = "", selection = TextRange.Zero)
private var imeOptions = ImeOptions.Default
private var ic: RecordingInputConnection? = null
private var focusedRect: android.graphics.Rect? = null
/**
* The editable buffer used for BaseInputConnection.
*/
private lateinit var imm: InputMethodManager
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
// focusedRect is null if there is not ongoing text input session. So safe to request
// latest focused rectangle whenever global layout has changed.
focusedRect?.let { view.requestRectangleOnScreen(it) }
}
init {
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View?) {
v?.rootView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
}
override fun onViewAttachedToWindow(v: View?) {
v?.rootView?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
}
})
}
/**
* Creates new input connection.
*/
fun createInputConnection(outAttrs: EditorInfo): InputConnection? {
if (!editorHasFocus) {
return null
}
fillEditorInfo(outAttrs)
return RecordingInputConnection(
initState = state,
autoCorrect = imeOptions.autoCorrect,
eventListener = object : InputEventListener {
override fun onEditOperations(editOps: List<EditOperation>) {
onEditCommand(editOps)
}
override fun onImeAction(imeAction: ImeAction) {
onImeActionPerformed(imeAction)
}
}
).also { ic = it }
}
/**
* Returns true if some editable component is focused.
*/
fun isEditorFocused(): Boolean = editorHasFocus
override fun startInput(
value: TextFieldValue,
imeOptions: ImeOptions,
onEditCommand: (List<EditOperation>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
) {
imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
editorHasFocus = true
state = value
this.imeOptions = imeOptions
this.onEditCommand = onEditCommand
this.onImeActionPerformed = onImeActionPerformed
view.post {
imm.restartInput(view)
imm.showSoftInput(view, 0)
}
}
override fun stopInput() {
editorHasFocus = false
onEditCommand = {}
onImeActionPerformed = {}
focusedRect = null
imm.restartInput(view)
editorHasFocus = false
}
override fun showSoftwareKeyboard() {
imm.showSoftInput(view, 0)
}
override fun hideSoftwareKeyboard() {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
override fun onStateUpdated(value: TextFieldValue) {
this.state = value
ic?.updateInputState(this.state, imm, view)
}
override fun notifyFocusedRect(rect: Rect) {
focusedRect = android.graphics.Rect(
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) {
view.requestRectangleOnScreen(focusedRect)
}
}
/**
* Fills necessary info of EditorInfo.
*/
private fun fillEditorInfo(outInfo: EditorInfo) {
outInfo.imeOptions = when (imeOptions.imeAction) {
ImeAction.Unspecified -> {
if (imeOptions.singleLine) {
// this is the last resort to enable single line
// Android IME still show return key even if multi line is not send
// TextView.java#onCreateInputConnection
EditorInfo.IME_ACTION_DONE
} else {
EditorInfo.IME_ACTION_UNSPECIFIED
}
}
ImeAction.NoAction -> EditorInfo.IME_ACTION_NONE
ImeAction.Go -> EditorInfo.IME_ACTION_GO
ImeAction.Next -> EditorInfo.IME_ACTION_NEXT
ImeAction.Previous -> EditorInfo.IME_ACTION_PREVIOUS
ImeAction.Search -> EditorInfo.IME_ACTION_SEARCH
ImeAction.Send -> EditorInfo.IME_ACTION_SEND
ImeAction.Done -> EditorInfo.IME_ACTION_DONE
else -> throw IllegalArgumentException(
"Unknown ImeAction: ${imeOptions.imeAction}"
)
}
when (imeOptions.keyboardType) {
KeyboardType.Text -> outInfo.inputType = InputType.TYPE_CLASS_TEXT
KeyboardType.Ascii -> {
outInfo.inputType = InputType.TYPE_CLASS_TEXT
outInfo.imeOptions = outInfo.imeOptions or EditorInfo.IME_FLAG_FORCE_ASCII
}
KeyboardType.Number -> outInfo.inputType = InputType.TYPE_CLASS_NUMBER
KeyboardType.Phone -> outInfo.inputType = InputType.TYPE_CLASS_PHONE
KeyboardType.Uri ->
outInfo.inputType = InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI
KeyboardType.Email ->
outInfo.inputType =
InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
KeyboardType.Password -> {
outInfo.inputType =
InputType.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
}
KeyboardType.NumberPassword -> {
outInfo.inputType =
InputType.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD
}
else -> throw IllegalArgumentException(
"Unknown KeyboardType: ${imeOptions.keyboardType}"
)
}
if (!imeOptions.singleLine) {
if (hasFlag(outInfo.inputType, InputType.TYPE_CLASS_TEXT)) {
// TextView.java#setInputTypeSingleLine
outInfo.inputType = outInfo.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
// adding this flag caused b/171598334, leaving here on purpose for future reference
// TextView.java#onCreateInputConnection
// outInfo.imeOptions = outInfo.imeOptions or EditorInfo.IME_FLAG_NO_ENTER_ACTION
}
}
if (hasFlag(outInfo.inputType, InputType.TYPE_CLASS_TEXT)) {
when (imeOptions.capitalization) {
KeyboardCapitalization.None -> {
/* do nothing */
}
KeyboardCapitalization.Characters -> {
outInfo.inputType = outInfo.inputType or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
}
KeyboardCapitalization.Words -> {
outInfo.inputType = outInfo.inputType or InputType.TYPE_TEXT_FLAG_CAP_WORDS
}
KeyboardCapitalization.Sentences -> {
outInfo.inputType = outInfo.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
}
}
if (imeOptions.autoCorrect) {
outInfo.inputType = outInfo.inputType or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
}
}
outInfo.imeOptions = outInfo.imeOptions or EditorInfo.IME_FLAG_NO_FULLSCREEN
}
private fun hasFlag(bits: Int, flag: Int): Boolean = (bits and flag) == flag
}