1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.text.input.internal
18
19 import android.content.Context
20 import android.os.Build
21 import android.view.KeyEvent
22 import android.view.View
23 import android.view.inputmethod.BaseInputConnection
24 import android.view.inputmethod.CursorAnchorInfo
25 import android.view.inputmethod.ExtractedText
26 import android.view.inputmethod.InputMethodManager
27 import androidx.annotation.RequiresApi
28 import androidx.annotation.VisibleForTesting
29 import androidx.core.view.SoftwareKeyboardControllerCompat
30 import org.jetbrains.annotations.TestOnly
31
32 /**
33 * Compatibility interface for [InputMethodManager] to use in Compose text input systems.
34 *
35 * This interface is responsible for handling the calls made to platform InputMethodManager in
36 * Android. There are different ways to show and hide software keyboard depending on API level.
37 *
38 * This interface also allows us to fake out the IMM for testing. For that reason, it should match
39 * the relevant platform [InputMethodManager] APIs as closely as possible.
40 */
41 internal interface ComposeInputMethodManager {
42 fun restartInput()
43
44 fun showSoftInput()
45
46 fun hideSoftInput()
47
48 fun updateExtractedText(token: Int, extractedText: ExtractedText)
49
50 fun updateSelection(
51 selectionStart: Int,
52 selectionEnd: Int,
53 compositionStart: Int,
54 compositionEnd: Int
55 )
56
57 fun updateCursorAnchorInfo(info: CursorAnchorInfo)
58
59 /**
60 * Sends a [KeyEvent] originated from an InputMethod to the Window. This is a necessary
61 * delegation when the InputConnection itself does not handle the received event.
62 */
63 fun sendKeyEvent(event: KeyEvent)
64
65 /** Signal the IME to start stylus handwriting. */
66 fun startStylusHandwriting()
67
68 fun prepareStylusHandwritingDelegation()
69
70 fun acceptStylusHandwritingDelegation()
71 }
72
73 /**
74 * Creates a new instance of [ComposeInputMethodManager].
75 *
76 * The value returned by this function can be changed for tests by calling
77 * [overrideComposeInputMethodManagerFactoryForTests].
78 */
ComposeInputMethodManagernull79 internal fun ComposeInputMethodManager(view: View): ComposeInputMethodManager =
80 ComposeInputMethodManagerFactory(view)
81
82 /** This lets us swap out the implementation in our own tests. */
83 private var ComposeInputMethodManagerFactory: (View) -> ComposeInputMethodManager = { view ->
84 when {
85 Build.VERSION.SDK_INT >= 34 -> ComposeInputMethodManagerImplApi34(view)
86 Build.VERSION.SDK_INT >= 24 -> ComposeInputMethodManagerImplApi24(view)
87 else -> ComposeInputMethodManagerImplApi21(view)
88 }
89 }
90
91 /**
92 * Sets the factory used by [ComposeInputMethodManager] to create instances and returns the previous
93 * factory.
94 *
95 * Any test that calls this should call it again to restore the factory after the test finishes, to
96 * avoid breaking unrelated tests.
97 */
98 @TestOnly
99 @VisibleForTesting
overrideComposeInputMethodManagerFactoryForTestsnull100 internal fun overrideComposeInputMethodManagerFactoryForTests(
101 factory: (View) -> ComposeInputMethodManager
102 ): (View) -> ComposeInputMethodManager {
103 val oldFactory = ComposeInputMethodManagerFactory
104 ComposeInputMethodManagerFactory = factory
105 return oldFactory
106 }
107
108 private abstract class ComposeInputMethodManagerImpl(protected val view: View) :
109 ComposeInputMethodManager {
110
111 private var imm: InputMethodManager? = null
112
113 private val softwareKeyboardControllerCompat = SoftwareKeyboardControllerCompat(view)
114
restartInputnull115 override fun restartInput() {
116 requireImm().restartInput(view)
117 }
118
showSoftInputnull119 override fun showSoftInput() {
120 softwareKeyboardControllerCompat.show()
121 }
122
hideSoftInputnull123 override fun hideSoftInput() {
124 softwareKeyboardControllerCompat.hide()
125 }
126
updateExtractedTextnull127 override fun updateExtractedText(token: Int, extractedText: ExtractedText) {
128 requireImm().updateExtractedText(view, token, extractedText)
129 }
130
updateSelectionnull131 override fun updateSelection(
132 selectionStart: Int,
133 selectionEnd: Int,
134 compositionStart: Int,
135 compositionEnd: Int
136 ) {
137 requireImm()
138 .updateSelection(view, selectionStart, selectionEnd, compositionStart, compositionEnd)
139 }
140
updateCursorAnchorInfonull141 override fun updateCursorAnchorInfo(info: CursorAnchorInfo) {
142 requireImm().updateCursorAnchorInfo(view, info)
143 }
144
startStylusHandwritingnull145 override fun startStylusHandwriting() {
146 // stylus handwriting is only supported after Android U.
147 }
148
prepareStylusHandwritingDelegationnull149 override fun prepareStylusHandwritingDelegation() {
150 // stylus handwriting is only supported after Android U.
151 }
152
acceptStylusHandwritingDelegationnull153 override fun acceptStylusHandwritingDelegation() {
154 // stylus handwriting is only supported after Android U.
155 }
156
requireImmnull157 protected fun requireImm(): InputMethodManager = imm ?: createImm().also { imm = it }
158
createImmnull159 private fun createImm() =
160 view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
161 }
162
163 private open class ComposeInputMethodManagerImplApi21(view: View) :
164 ComposeInputMethodManagerImpl(view) {
165
166 /**
167 * Prior to API24, the safest way to delegate IME originated KeyEvents to the window was through
168 * BaseInputConnection.
169 */
170 private var baseInputConnection: BaseInputConnection? = null
171
172 override fun sendKeyEvent(event: KeyEvent) {
173 val baseInputConnection =
174 baseInputConnection
175 ?: BaseInputConnection(view, false).also { baseInputConnection = it }
176 baseInputConnection.sendKeyEvent(event)
177 }
178 }
179
180 @RequiresApi(24)
181 private open class ComposeInputMethodManagerImplApi24(view: View) :
182 ComposeInputMethodManagerImplApi21(view) {
183
sendKeyEventnull184 override fun sendKeyEvent(event: KeyEvent) {
185 requireImm().dispatchKeyEventFromInputMethod(view, event)
186 }
187 }
188
189 @RequiresApi(34)
190 private open class ComposeInputMethodManagerImplApi34(view: View) :
191 ComposeInputMethodManagerImplApi24(view) {
startStylusHandwritingnull192 override fun startStylusHandwriting() {
193 requireImm().startStylusHandwriting(view)
194 }
195
prepareStylusHandwritingDelegationnull196 override fun prepareStylusHandwritingDelegation() {
197 requireImm().prepareStylusHandwritingDelegation(view)
198 }
199
acceptStylusHandwritingDelegationnull200 override fun acceptStylusHandwritingDelegation() {
201 requireImm().acceptStylusHandwritingDelegation(view)
202 }
203 }
204