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