1 /*
<lambda>null2  * Copyright 2023 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.platform
20 
21 import android.view.View
22 import android.view.inputmethod.EditorInfo
23 import android.view.inputmethod.InputConnection
24 import androidx.compose.runtime.collection.mutableVectorOf
25 import androidx.compose.ui.InternalComposeUiApi
26 import androidx.compose.ui.SessionMutex
27 import androidx.compose.ui.node.Owner
28 import androidx.compose.ui.node.WeakReference
29 import androidx.compose.ui.text.InternalTextApi
30 import androidx.compose.ui.text.input.NullableInputConnectionWrapper
31 import androidx.compose.ui.text.input.TextInputService
32 import kotlinx.coroutines.CoroutineScope
33 import kotlinx.coroutines.cancel
34 import kotlinx.coroutines.suspendCancellableCoroutine
35 
36 /**
37  * Manages a top-level input session, as created by [Owner.textInputSession].
38  *
39  * On Android there are three levels of input sessions:
40  * 1. [PlatformTextInputModifierNode.establishTextInputSession]: The app is performing some
41  *    initialization before requesting the keyboard.
42  * 2. [PlatformTextInputSession.startInputMethod]: The app has requested the keyboard with a
43  *    particular implementation for [View.onCreateInputConnection] represented by a
44  *    [PlatformTextInputMethodRequest].
45  * 3. [View.onCreateInputConnection]: The system has responded to the keyboard request by asking the
46  *    view for a new [InputConnection].
47  *
48  * Each of these sessions is a parent of the next, in terms of lifetime and cancellation.
49  *
50  * This class manages child sessions started by [startInputMethod]. Each child session is
51  * represented by an [InputMethodSession], which in turn coordinates between multiple calls to
52  * [createInputConnection] for a single session.
53  */
54 @OptIn(InternalTextApi::class, InternalComposeUiApi::class)
55 internal class AndroidPlatformTextInputSession(
56     override val view: View,
57     private val textInputService: TextInputService,
58     private val coroutineScope: CoroutineScope
59 ) : PlatformTextInputSessionScope, CoroutineScope by coroutineScope {
60     /** Coordinates between calls to [startInputMethod]. */
61     private val methodSessionMutex = SessionMutex<InputMethodSession>()
62 
63     /**
64      * Returns true if [startInputMethod] has been called and is ready to respond to
65      * [createInputConnection].
66      */
67     val isReadyForConnection: Boolean
68         get() = methodSessionMutex.currentSession?.isActive == true
69 
70     override suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing =
71         methodSessionMutex.withSessionCancellingPrevious(
72             sessionInitializer = {
73                 InputMethodSession(
74                     request = request,
75                     onAllConnectionsClosed = { coroutineScope.cancel() }
76                 )
77             }
78         ) { methodSession ->
79             @Suppress("RemoveExplicitTypeArguments")
80             suspendCancellableCoroutine<Nothing> { continuation ->
81                 // Show the keyboard and ask the IMM to restart input.
82                 textInputService.startInput()
83 
84                 // The cleanup needs to be executed synchronously, otherwise the stopInput call
85                 // might come too late and end up overriding the next field's startInput. This
86                 // is prevented by the queuing in TextInputService, but can still happen when
87                 // focus transfers from a compose text field to a view-based text field
88                 // (EditText).
89                 continuation.invokeOnCancellation {
90                     methodSession.dispose()
91 
92                     // If this session was cancelled because another session was requested, this
93                     // call will be a noop.
94                     textInputService.stopInput()
95                 }
96             }
97         }
98 
99     /**
100      * Creates a new [InputConnection] using the current [InputMethodSession]'s request, or returns
101      * null if there's no active [InputMethodSession] (i.e. [startInputMethod] hasn't been called
102      * yet or the session is being torn down).
103      */
104     fun createInputConnection(outAttrs: EditorInfo): InputConnection? =
105         methodSessionMutex.currentSession?.createInputConnection(outAttrs)
106 }
107 
108 /**
109  * Coordinates between calls to [View.onCreateInputConnection] for a single
110  * [AndroidPlatformTextInputSession]'s input method session. Instances of this class correspond to
111  * calls to [AndroidPlatformTextInputSession.startInputMethod]. This class ensures that old
112  * connections are disposed before new ones are created.
113  *
114  * @param onAllConnectionsClosed Called when all created [InputConnection]s receive
115  *   [InputConnection.closeConnection] call.
116  */
117 private class InputMethodSession(
118     private val request: PlatformTextInputMethodRequest,
119     private val onAllConnectionsClosed: () -> Unit
120 ) {
121     private val lock = Any()
122     private var connections = mutableVectorOf<WeakReference<NullableInputConnectionWrapper>>()
123     private var disposed = false
124 
125     val isActive: Boolean
126         get() = !disposed
127 
128     /**
129      * Creates a new [InputConnection] and initializes [outAttrs] by calling this session's
130      * [PlatformTextInputMethodRequest.createInputConnection]. If a previous connection is active,
131      * it will be disposed (closed and reference cleared) before creating the new one.
132      *
133      * Returns null if [dispose] has been called.
134      */
createInputConnectionnull135     fun createInputConnection(outAttrs: EditorInfo): InputConnection? {
136         synchronized(lock) {
137             if (disposed) return null
138 
139             // Do not manually dispose a previous InputConnection until it's collected by the GC or
140             // an explicit call is received to its `onConnectionClosed` callback.
141 
142             val connectionDelegate = request.createInputConnection(outAttrs)
143             return NullableInputConnectionWrapper(
144                     delegate = connectionDelegate,
145                     onConnectionClosed = { closedConnection ->
146                         // We should not cancel any ongoing input session because connection is
147                         // closed
148                         // from InputConnection. This may happen at any time and does not indicate
149                         // whether the system is trying to stop an input session. The platform may
150                         // request a new InputConnection immediately after closing this one.
151                         // Instead we should just clear all the resources used by this
152                         // inputConnection,
153                         // because the platform guarantees that it will never reuse a closed
154                         // connection.
155                         closedConnection.disposeDelegate()
156                         val removeIndex = connections.indexOfFirst { it == closedConnection }
157                         if (removeIndex >= 0) connections.removeAt(removeIndex)
158                         if (connections.isEmpty()) {
159                             onAllConnectionsClosed()
160                         }
161                     }
162                 )
163                 .also { connections.add(WeakReference(it)) }
164         }
165     }
166 
167     /**
168      * Disposes the current [InputConnection]. After calling this method, all future calls to
169      * [createInputConnection] will return null.
170      *
171      * This function is only called from coroutine cancellation routine so it's not required to
172      * cancel the coroutine from here.
173      */
disposenull174     fun dispose() {
175         synchronized(lock) {
176             // Manually close the delegate in case the system forgets to.
177             disposed = true
178             connections.forEach { it.get()?.disposeDelegate() }
179             connections.clear()
180         }
181     }
182 }
183