I am trying to build an IME (input method editor) for Android. I know that I have to create a class that extends InputMethodService, to have access to the getCurrentInputConnection method. My understanding is that this returns me to the currently focused text field or null if there isn't.
Then I know I have to do something like this:
val focusedTextField = currentInputConnection ?: return
The problem is that I always get null as a result. My theory is that the Text Editor (currently focused Text Field) doesn't recognize my app as an IME or maybe "doesn't realize" that it is being focused. So maybe I have to provide more information. I already checked the manifest where I declare the service and provide the metadata and everything seems to be correct. The res/xml/method.xml file is correct.
this is the manifest file. I have been told that since android 11 we have to ask for location permission to use services
<service
android:name=".IMEService"
android:label="Amazing Keyboard"
android:foregroundServiceType="location"
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/method" />
</service>
This is the method file
<?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.example.amazingkeyboard.MainActivity">
<subtype
android:name = "my app"
android:label="English (U.S)"
android:imeSubtypeLocale="en_US"
android:imeSubtypeMode="keyboard"/>
</input-method>
This is what I am doing, I am using jetpack compose, but that is not the problem, because I already tried to return an xml view and I always have the error
class IMEService : InputMethodService(), LifecycleOwner,
ViewModelStoreOwner, SavedStateRegistryOwner {
fun sendText(text : CharSequence, newCursorPosition : Int) {
val focusedTextField = currentInputConnection ?: return //always returns null
focusedTextField.commitText(text, newCursorPosition)
}
...
}
This is where I call the method
val connection = IMEService()
@Composable fun TestKey(modifier: Modifier = Modifier) {
Key(
modifier = modifier .clickable {
connection.sendText(unicodeToString(0x1F436), 1)
}...
when I remove the validation. As I said above, the problem is that I always get null. Obviously if I do the validation, there will be no error, but I can't send either (because I always have null)
// val focusedTextField = currentInputConnection ?: return
val focusedTextField = currentInputConnection
I have this error.
java.lang.NullPointerException:
Attempt to invoke interface method 'boolean android.view.inputmethod.InputConnection.commitText(java.lang.CharSequence, int)' on a null object reference
at com.chrrissoft.amazingkeyboard.IMEService.sendText(IMEService.kt:21)
at com.chrrissoft.amazingkeyboard.composables.GeneralKeysKt$TestKey$1.invoke(generalKeys.kt:32)
at com.chrrissoft.amazingkeyboard.composables.GeneralKeysKt$TestKey$1.invoke(generalKeys.kt:31)
Here is the complete project, in case you want to review it.
You are getting NullPointerException
on the second line of the following code:
val focusedTextField = currentInputConnection
focusedTextField.commitText(text, newCursorPosition)
because the currently active InputConnection
isn't bound to the input method, and that's why currentInputConnection
is null.
There is a onBindInput
method in InputConnection
, which is called when a new client has bound to the input method. Upon this call you know that currentInputConnection
return valid object.
So before using currentInputConnection
client should be bound to the input method.
To use IMEService
's public methods outside of its class scope, you need to have an instance of the bound service. Digging into your sources on GitHub it seems the problem is easy to solve by passing IMEService
to the TestKey()
function. The whole code will look something like this:
@Composable
fun TestKey(modifier: Modifier = Modifier, connection: IMEService) {
Key(
modifier = modifier
//...
.clickable {
connection.sendText(unicodeToString(0x1F383), 1)
}
) {
Icon(/*...*/)
}
}
@Composable
fun EmojiLayout(navController: NavHostController, connection: IMEService) {
val (currentPage, onPageChange) = remember {
mutableStateOf(EmoticonsAndEmotionsPage)
}
Column(modifier = Modifier.fillMaxSize()) {
EmojiPage(
//...
)
Row(
//...
) {
//...
TestKey(Modifier.weight(1f), connection)
}
}
}
@Composable
fun KeyboardScreen(connection: IMEService) {
AmazingKeyboardTheme() {
Column(
//...
) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "qwertyLayout") {
//...
composable("emojiLayout") { EmojiLayout(navController, connection) }
}
}
}
}
class AndroidKeyboardView(context: Context) : FrameLayout(context) {
constructor(service: IMEService) : this(service as Context) {
inflate(service, R.layout.keyboard_view, this)
findViewById<ComposeView>(R.id.compose_view).setContent {
KeyboardScreen(connection = service)
}
}
}
IMEService
class stays the same.