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