1 /*
<lambda>null2  * Copyright 2019 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 @file:Suppress("DEPRECATION")
18 
19 package androidx.compose.ui.text.input
20 
21 import androidx.annotation.RestrictTo
22 import androidx.compose.ui.geometry.Rect
23 import androidx.compose.ui.graphics.Matrix
24 import androidx.compose.ui.text.AtomicReference
25 import androidx.compose.ui.text.InternalTextApi
26 import androidx.compose.ui.text.TextLayoutResult
27 
28 /**
29  * Handles communication with the IME. Informs about the IME changes via [EditCommand]s and provides
30  * utilities for working with software keyboard.
31  *
32  * This class is responsible for ensuring there is only one open [TextInputSession] which will
33  * interact with software keyboards. Start new a TextInputSession by calling [startInput] and close
34  * it with [stopInput].
35  */
36 // Open for testing purposes.
37 @Deprecated("Use PlatformTextInputModifierNode instead.")
38 open class TextInputService(private val platformTextInputService: PlatformTextInputService) {
39     private val _currentInputSession: AtomicReference<TextInputSession?> = AtomicReference(null)
40 
41     internal val currentInputSession: TextInputSession?
42         get() = _currentInputSession.get()
43 
44     /**
45      * Start text input session for given client.
46      *
47      * If there is a previous [TextInputSession] open, it will immediately be closed by this call to
48      * [startInput].
49      *
50      * @param value initial [TextFieldValue]
51      * @param imeOptions IME configuration
52      * @param onEditCommand callback to inform about changes requested by IME
53      * @param onImeActionPerformed callback to inform if an IME action such as [ImeAction.Done] etc
54      *   occurred.
55      */
56     open fun startInput(
57         value: TextFieldValue,
58         imeOptions: ImeOptions,
59         onEditCommand: (List<EditCommand>) -> Unit,
60         onImeActionPerformed: (ImeAction) -> Unit
61     ): TextInputSession {
62         platformTextInputService.startInput(value, imeOptions, onEditCommand, onImeActionPerformed)
63         val nextSession = TextInputSession(this, platformTextInputService)
64         _currentInputSession.set(nextSession)
65         return nextSession
66     }
67 
68     /**
69      * Restart input and show the keyboard. This should only be called when starting a new
70      * `PlatformTextInputModifierNode.textInputSession`.
71      */
72     @InternalTextApi
73     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
74     fun startInput() {
75         platformTextInputService.startInput()
76         val nextSession = TextInputSession(this, platformTextInputService)
77         _currentInputSession.set(nextSession)
78     }
79 
80     /**
81      * Stop text input session.
82      *
83      * If the [session] is not the currently open session, no action will occur.
84      *
85      * @param session the session returned by [startInput] call.
86      */
87     open fun stopInput(session: TextInputSession) {
88         if (_currentInputSession.compareAndSet(session, null)) {
89             platformTextInputService.stopInput()
90         }
91     }
92 
93     @InternalTextApi
94     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
95     fun stopInput() {
96         // This is a direct stop call, there's no need to compare the current input session.
97         _currentInputSession.set(null)
98         platformTextInputService.stopInput()
99     }
100 
101     /**
102      * Request showing onscreen keyboard.
103      *
104      * This call will be ignored if there is not an open [TextInputSession], as it means there is
105      * nothing that will accept typed input. The most common way to open a TextInputSession is to
106      * set the focus to an editable text composable.
107      *
108      * There is no guarantee that the keyboard will be shown. The software keyboard or system
109      * service may silently ignore this request.
110      */
111     @Deprecated(
112         message =
113             "Use SoftwareKeyboardController.show or " +
114                 "TextInputSession.showSoftwareKeyboard instead.",
115         replaceWith = ReplaceWith("textInputSession.showSoftwareKeyboard()")
116     )
117     // TODO(b/183448615) @InternalTextApi
118     fun showSoftwareKeyboard() {
119         if (currentInputSession != null) {
120             platformTextInputService.showSoftwareKeyboard()
121         }
122     }
123 
124     /** Hide onscreen keyboard. */
125     @Deprecated(
126         message =
127             "Use SoftwareKeyboardController.hide or " +
128                 "TextInputSession.hideSoftwareKeyboard instead.",
129         replaceWith = ReplaceWith("textInputSession.hideSoftwareKeyboard()")
130     )
131     // TODO(b/183448615) @InternalTextApi
132     fun hideSoftwareKeyboard(): Unit = platformTextInputService.hideSoftwareKeyboard()
133 }
134 
135 /**
136  * Represents a input session for interactions between a soft keyboard and editable text.
137  *
138  * This session may be closed at any time by [TextInputService] or by calling [dispose], after which
139  * [isOpen] will return false and all further calls will have no effect.
140  */
141 @Deprecated("Use PlatformTextInputModifierNode instead.")
142 class TextInputSession(
143     private val textInputService: TextInputService,
144     private val platformTextInputService: PlatformTextInputService
145 ) {
146     /**
147      * If this session is currently open.
148      *
149      * A session may be closed at any time by [TextInputService] or by calling [dispose].
150      */
151     val isOpen: Boolean
152         get() = textInputService.currentInputSession == this
153 
154     /**
155      * Close this input session.
156      *
157      * All further calls to this object will have no effect, and [isOpen] will return false.
158      *
159      * Note, [TextInputService] may also close this input session at any time without calling
160      * dispose. Calling dispose after this session has been closed has no effect.
161      */
disposenull162     fun dispose() {
163         textInputService.stopInput(this)
164     }
165 
166     /**
167      * Execute [block] if [isOpen] is true.
168      *
169      * This function will only check [isOpen] once, and may execute the action after the input
170      * session closes in the case of concurrent execution.
171      *
172      * @param block action to take if isOpen
173      * @return true if an action was performed
174      */
ensureOpenSessionnull175     private inline fun ensureOpenSession(block: () -> Unit): Boolean {
176         return isOpen.also { applying ->
177             if (applying) {
178                 block()
179             }
180         }
181     }
182 
183     /**
184      * Notify the focused rectangle to the system.
185      *
186      * The system can ignore this information or use it to for additional functionality.
187      *
188      * For example, desktop systems show a popup near the focused input area (for some languages).
189      *
190      * If the session is not open, no action will be performed.
191      *
192      * @param rect the rectangle that describes the boundaries on the screen that requires focus
193      * @return false if this session expired and no action was performed
194      */
<lambda>null195     fun notifyFocusedRect(rect: Rect): Boolean = ensureOpenSession {
196         platformTextInputService.notifyFocusedRect(rect)
197     }
198 
199     /**
200      * Notify the input service of layout and position changes.
201      *
202      * @param textFieldValue the text field's [TextFieldValue]
203      * @param offsetMapping the offset mapping for the visual transformation
204      * @param textLayoutResult the text field's [TextLayoutResult]
205      * @param textFieldToRootTransform function that modifies a matrix to be a transformation matrix
206      *   from local coordinates to the root composable coordinates
207      * @param innerTextFieldBounds visible bounds of the text field in text layout coordinates, or
208      *   an empty rectangle if the text field is not visible
209      * @param decorationBoxBounds visible bounds of the decoration box in text layout coordinates,
210      *   or an empty rectangle if the decoration box is not visible
211      */
updateTextLayoutResultnull212     fun updateTextLayoutResult(
213         textFieldValue: TextFieldValue,
214         offsetMapping: OffsetMapping,
215         textLayoutResult: TextLayoutResult,
216         textFieldToRootTransform: (Matrix) -> Unit,
217         innerTextFieldBounds: Rect,
218         decorationBoxBounds: Rect
219     ) = ensureOpenSession {
220         platformTextInputService.updateTextLayoutResult(
221             textFieldValue,
222             offsetMapping,
223             textLayoutResult,
224             textFieldToRootTransform,
225             innerTextFieldBounds,
226             decorationBoxBounds
227         )
228     }
229 
230     /**
231      * Notify IME about the new [TextFieldValue] and latest state of the editing buffer. [oldValue]
232      * is the state of the buffer before the changes applied by the [newValue].
233      *
234      * [oldValue] represents the changes that was requested by IME on the buffer, and [newValue] is
235      * the final state of the editing buffer that was requested by the application. In cases where
236      * [oldValue] is not equal to [newValue], it would mean the IME suggested value is rejected, and
237      * the IME connection will be restarted with the newValue.
238      *
239      * If the session is not open, action will be performed.
240      *
241      * @param oldValue the value that was requested by IME on the buffer
242      * @param newValue final state of the editing buffer that was requested by the application
243      * @return false if this session expired and no action was performed
244      */
updateStatenull245     fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue): Boolean =
246         ensureOpenSession {
247             platformTextInputService.updateState(oldValue, newValue)
248         }
249 
250     /**
251      * Request showing onscreen keyboard.
252      *
253      * This call will have no effect if this session is not open.
254      *
255      * This should be used instead of [TextInputService.showSoftwareKeyboard] when implementing a
256      * new editable text composable to show the keyboard in response to events related to that
257      * composable.
258      *
259      * There is no guarantee that the keyboard will be shown. The software keyboard or system
260      * service may silently ignore this request.
261      *
262      * @return false if this session expired and no action was performed
263      */
264     // TODO(b/241399013) Deprecate when out of API freeze.
265     // @Deprecated("Use SoftwareKeyboardController.show() instead.")
<lambda>null266     fun showSoftwareKeyboard(): Boolean = ensureOpenSession {
267         platformTextInputService.showSoftwareKeyboard()
268     }
269 
270     /**
271      * Hide onscreen keyboard for a specific [TextInputSession].
272      *
273      * This call will have no effect if this session is not open.
274      *
275      * This should be used instead of [TextInputService.showSoftwareKeyboard] when implementing a
276      * new editable text composable to hide the keyboard in response to events related to that
277      * composable.
278      *
279      * @return false if this session expired and no action was performed
280      */
281     // TODO(b/241399013) Deprecate when out of API freeze.
282     // @Deprecated("Use SoftwareKeyboardController.hide() instead.")
<lambda>null283     fun hideSoftwareKeyboard(): Boolean = ensureOpenSession {
284         platformTextInputService.hideSoftwareKeyboard()
285     }
286 }
287 
288 /** Platform specific text input service. */
289 @Deprecated("Use PlatformTextInputModifierNode instead.")
290 interface PlatformTextInputService {
291     /**
292      * Start text input session for given client.
293      *
294      * @see TextInputService.startInput
295      */
startInputnull296     fun startInput(
297         value: TextFieldValue,
298         imeOptions: ImeOptions,
299         onEditCommand: (List<EditCommand>) -> Unit,
300         onImeActionPerformed: (ImeAction) -> Unit
301     )
302 
303     /**
304      * Restart input and show the keyboard. This should only be called when starting a new
305      * `PlatformTextInputModifierNode.textInputSession`.
306      *
307      * @see TextInputService.startInput
308      */
309     fun startInput() {}
310 
311     /**
312      * Stop text input session.
313      *
314      * @see TextInputService.stopInput
315      */
stopInputnull316     fun stopInput()
317 
318     /**
319      * Request showing onscreen keyboard
320      *
321      * There is no guarantee nor callback of the result of this API.
322      *
323      * @see TextInputService.showSoftwareKeyboard
324      */
325     fun showSoftwareKeyboard()
326 
327     /**
328      * Hide software keyboard
329      *
330      * @see TextInputService.hideSoftwareKeyboard
331      */
332     fun hideSoftwareKeyboard()
333 
334     /**
335      * Notify the new editor model to IME.
336      *
337      * @see TextInputSession.updateState
338      */
339     fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue)
340 
341     /**
342      * Notify the focused rectangle to the system.
343      *
344      * The system can ignore this information or use it to for additional functionality.
345      *
346      * For example, desktop systems show a popup near the focused input area (for some languages).
347      */
348     // TODO(b/262648050) Try to find a better API.
349     fun notifyFocusedRect(rect: Rect) {}
350 
351     /**
352      * Notify the input service of layout and position changes.
353      *
354      * @see TextInputSession.updateTextLayoutResult
355      */
updateTextLayoutResultnull356     fun updateTextLayoutResult(
357         textFieldValue: TextFieldValue,
358         offsetMapping: OffsetMapping,
359         textLayoutResult: TextLayoutResult,
360         textFieldToRootTransform: (Matrix) -> Unit,
361         innerTextFieldBounds: Rect,
362         decorationBoxBounds: Rect
363     ) {}
364 }
365