ComposeInputMethodManager.kt

/*
 * 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.content.Context
import android.os.Build
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.BaseInputConnection
import android.view.inputmethod.ExtractedText
import android.view.inputmethod.InputMethodManager
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.view.SoftwareKeyboardControllerCompat
import org.jetbrains.annotations.TestOnly

/**
 * Compatibility interface for [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.
 *
 * This interface also allows us to fake out the IMM for testing. For that reason, it should match
 * the relevant platform [InputMethodManager] APIs as closely as possible.
 */
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)
}

/**
 * Creates a new instance of [ComposeInputMethodManager].
 *
 * The value returned by this function can be changed for tests by calling
 * [overrideComposeInputMethodManagerFactoryForTests].
 */
internal fun ComposeInputMethodManager(view: View): ComposeInputMethodManager =
    ComposeInputMethodManagerFactory(view)

/** This lets us swap out the implementation in our own tests. */
private var ComposeInputMethodManagerFactory: (View) -> ComposeInputMethodManager = { view ->
    when {
        Build.VERSION.SDK_INT >= 24 -> ComposeInputMethodManagerImplApi24(view)
        else -> ComposeInputMethodManagerImplApi21(view)
    }
}

/**
 * Sets the factory used by [ComposeInputMethodManager] to create instances and returns the previous
 * factory.
 *
 * Any test that calls this should call it again to restore the factory after the test finishes, to
 * avoid breaking unrelated tests.
 */
@TestOnly
@VisibleForTesting
internal fun overrideComposeInputMethodManagerFactoryForTests(
    factory: (View) -> ComposeInputMethodManager
): (View) -> ComposeInputMethodManager {
    val oldFactory = ComposeInputMethodManagerFactory
    ComposeInputMethodManagerFactory = factory
    return oldFactory
}

private abstract class ComposeInputMethodManagerImpl(protected val view: View) :
    ComposeInputMethodManager {

    private var imm: InputMethodManager? = null

    private val softwareKeyboardControllerCompat =
        SoftwareKeyboardControllerCompat(view)

    override fun restartInput() {
        requireImm().restartInput(view)
    }

    override fun showSoftInput() {
        softwareKeyboardControllerCompat.show()
    }

    override fun hideSoftInput() {
        softwareKeyboardControllerCompat.hide()
    }

    override fun updateExtractedText(
        token: Int,
        extractedText: ExtractedText
    ) {
        requireImm().updateExtractedText(view, token, extractedText)
    }

    override fun updateSelection(
        selectionStart: Int,
        selectionEnd: Int,
        compositionStart: Int,
        compositionEnd: Int
    ) {
        requireImm().updateSelection(
            view,
            selectionStart,
            selectionEnd,
            compositionStart,
            compositionEnd
        )
    }

    protected fun requireImm(): InputMethodManager = imm ?: createImm().also { imm = it }

    private fun createImm() =
        view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}

private open class ComposeInputMethodManagerImplApi21(view: View) :
    ComposeInputMethodManagerImpl(view) {

    /**
     * Prior to API24, the safest way to delegate IME originated KeyEvents to the window was
     * through BaseInputConnection.
     */
    private var baseInputConnection: BaseInputConnection? = null

    override fun sendKeyEvent(event: KeyEvent) {
        val baseInputConnection = baseInputConnection
            ?: BaseInputConnection(view, false).also { baseInputConnection = it }
        baseInputConnection.sendKeyEvent(event)
    }
}

@RequiresApi(24)
private open class ComposeInputMethodManagerImplApi24(view: View) :
    ComposeInputMethodManagerImplApi21(view) {

    override fun sendKeyEvent(event: KeyEvent) {
        requireImm().dispatchKeyEventFromInputMethod(view, event)
    }
}