2

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.

Sergio
  • 27,326
  • 8
  • 128
  • 149
chrrissoft
  • 149
  • 1
  • 10

2 Answers2

2

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.

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • Hello. Thank you for taking the time to review and sorry for my delay. Unfortunately the code you wrote doesn't work, I had already tried something similar before. I was trying to link the service, but I couldn't find a way, so I decided to leave it for later and work on other parts of the app. I still don't know how to link the service, so if you have any idea how to do it, I would really appreciate if you tell me – chrrissoft Apr 09 '22 at 06:00
  • El error que genera el código es el siguiente `org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering File being compiled: AndroidKeyboardView.kt The root cause java.lang.RuntimeException was thrown at: org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:50) .... Caused by: java.lang.IllegalStateException: No mapping for symbol: VALUE_PARAMETER name:context index:0 type:com.chrrissoft.amazingkeyboard.IMEService` – chrrissoft Apr 09 '22 at 06:09
  • @chrrissoft please check my edited code for `AndroidKeyboardView` class. Basically, instead of `init` block you need to use a secondary constructor for `AndroidKeyboardView` and it will work. – Sergio Apr 10 '22 at 16:54
  • Hello. I already tried performing the five steps that you indicated and it did not work. I already tried setting the `kotlinCompilerExtensionVersion` manually as indicated in the answer, but it didn't solve the problem either. `kotlinCompilerVersion` is deprecated – chrrissoft Apr 10 '22 at 21:40
  • [here is the full error](https://gist.github.com/chrrissoft/1634cfb3949616e5ff203e64ace59241) – chrrissoft Apr 10 '22 at 21:41
  • I've edited my answer. Please check the code for AndroidKeyboardView class. You need to use a secondary constructor instead of init{} block. When you change init{} block on secondary constructor the build error will disappear. – Sergio Apr 11 '22 at 05:21
  • @chrrissoft you can also review and merge a pull request I've made into your repository with those changes I've described. Link to PR https://github.com/chrrissoft/AmazingKeyboard/pull/1 – Sergio Apr 11 '22 at 06:15
  • Thank you. Now it works, what do I have to do for you to get the reward. sorry I'm new to this – chrrissoft Apr 11 '22 at 18:01
  • You need to accept my answer by clicking the check mark to the left of my answer, thank you. – Sergio Apr 11 '22 at 18:14
0

I assume you followed this documentation and made all the necessary setups, including adding IMEService to the Android Manifest.

val connection = IMEService()

Unfortunately you can't use a Service like that, you can't create an instance of Service class by yourself and rely that it will work correctly. Services are created by Android system. If you want to use currentInputConnection property you should use it from within the IMEService, or you need somehow to pass an instance of it to the place where you want to use it, or to bind to it if you want to have an instance of IMEService service. Please refer also to these answers regarding how to bind to a Service.

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • So I have to use the method [onBindInput](https://developer.android.com/reference/android/inputmethodservice/InputMethodService#onBindInput()) sorry I'm new to this service, and android in general. – chrrissoft Apr 02 '22 at 16:08
  • If `onBindInput` is called, it means you will have access to `currentInputConnection` – Sergio Apr 02 '22 at 16:14