/*
* 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.compose.foundation.text2.input.internal
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.KeyEvent
import android.view.View
import android.view.Window
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.ExtractedText
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
/**
* Compatibility interface for [android.view.inputmethod.InputMethodManager] to use in Compose text
* input systems.
*
* This interface is responsible for handling the calls made to platform InputMethodManager in
* Android. There are different ways to show and hide software keyboard depending on API level.
*/
internal interface ComposeInputMethodManager {
fun restartInput()
fun showSoftInput()
fun hideSoftInput()
fun updateExtractedText(
token: Int,
extractedText: ExtractedText
)
fun updateSelection(
selectionStart: Int,
selectionEnd: Int,
compositionStart: Int,
compositionEnd: Int
)
/**
* Sends a [KeyEvent] originated from an InputMethod to the Window. This is a necessary
* delegation when the InputConnection itself does not handle the received event.
*/
fun sendKeyEvent(event: KeyEvent)
}
internal fun ComposeInputMethodManager(view: View): ComposeInputMethodManager {
return ComposeInputMethodManagerImpl(view)
}
private class ComposeInputMethodManagerImpl(private val view: View) : ComposeInputMethodManager {
private val imm by lazy(LazyThreadSafetyMode.NONE) {
view.context.getSystemService(Context.INPUT_METHOD_SERVICE)
as android.view.inputmethod.InputMethodManager
}
private val helper = if (Build.VERSION.SDK_INT >= 30) {
ImmHelper30(view)
} else if (Build.VERSION.SDK_INT >= 24) {
ImmHelper24(view)
} else {
ImmHelper21(view)
}
override fun restartInput() {
imm.restartInput(view)
}
override fun showSoftInput() {
helper.showSoftInput(imm)
}
override fun hideSoftInput() {
helper.hideSoftInput(imm)
}
override fun updateExtractedText(
token: Int,
extractedText: ExtractedText
) {
imm.updateExtractedText(view, token, extractedText)
}
override fun sendKeyEvent(event: KeyEvent) {
helper.sendKeyEvent(imm, event)
}
override fun updateSelection(
selectionStart: Int,
selectionEnd: Int,
compositionStart: Int,
compositionEnd: Int
) {
imm.updateSelection(view, selectionStart, selectionEnd, compositionStart, compositionEnd)
}
}
private interface ImmHelper {
fun showSoftInput(imm: android.view.inputmethod.InputMethodManager)
fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager)
fun sendKeyEvent(imm: android.view.inputmethod.InputMethodManager, event: KeyEvent)
}
private class ImmHelper21(private val view: View) : ImmHelper {
/**
* Prior to API24, the safest way to delegate IME originated KeyEvents to the window was
* through BaseInputConnection.
*/
private val baseInputConnection by lazy(LazyThreadSafetyMode.NONE) {
BaseInputConnection(view, false)
}
@DoNotInline
override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
view.post {
imm.showSoftInput(view, 0)
}
}
@DoNotInline
override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
@DoNotInline
override fun sendKeyEvent(imm: android.view.inputmethod.InputMethodManager, event: KeyEvent) {
baseInputConnection.sendKeyEvent(event)
}
}
@RequiresApi(24)
private class ImmHelper24(private val view: View) : ImmHelper {
@DoNotInline
override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
view.post {
imm.showSoftInput(view, 0)
}
}
@DoNotInline
override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
@DoNotInline
override fun sendKeyEvent(imm: android.view.inputmethod.InputMethodManager, event: KeyEvent) {
imm.dispatchKeyEventFromInputMethod(view, event)
}
}
@RequiresApi(30)
private class ImmHelper30(private val view: View) : ImmHelper {
/**
* Get a [WindowInsetsControllerCompat] for the view. This returns a new instance every time,
* since the view may return null or not null at different times depending on window attach
* state.
*/
private val insetsControllerCompat
// This can return null when, for example, the view is not attached to a window.
get() = view.findWindow()?.let { WindowInsetsControllerCompat(it, view) }
/**
* This class falls back to the legacy implementation when the window insets controller isn't
* available.
*/
private val immHelper21: ImmHelper21
get() = _immHelper21 ?: ImmHelper21(view).also { _immHelper21 = it }
private var _immHelper21: ImmHelper21? = null
@DoNotInline
override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
insetsControllerCompat?.apply {
show(WindowInsetsCompat.Type.ime())
} ?: immHelper21.showSoftInput(imm)
}
@DoNotInline
override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
insetsControllerCompat?.apply {
hide(WindowInsetsCompat.Type.ime())
} ?: immHelper21.hideSoftInput(imm)
}
@DoNotInline
override fun sendKeyEvent(imm: android.view.inputmethod.InputMethodManager, event: KeyEvent) {
imm.dispatchKeyEventFromInputMethod(view, event)
}
// TODO(b/221889664) Replace with composition local when available.
private fun View.findWindow(): Window? =
(parent as? DialogWindowProvider)?.window
?: context.findWindow()
private tailrec fun Context.findWindow(): Window? =
when (this) {
is Activity -> window
is ContextWrapper -> baseContext.findWindow()
else -> null
}
}