add android keyboard language change detection

uses ContentObserver on Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE
to detect when the user switches keyboard language on Android.
deduplicates events like the iOS side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-03-01 10:15:28 +02:00
parent e8793e49c8
commit b1389cdf27
@@ -1,31 +1,68 @@
package expo.modules.keyboardlanguage
import android.database.ContentObserver
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoKeyboardLanguageModule : Module() {
private var subtypeObserver: ContentObserver? = null
private var lastLanguage: String? = null
override fun definition() =
ModuleDefinition {
Name("ExpoKeyboardLanguage")
Function("getCurrentKeyboardLanguage") {
val context = appContext.reactContext ?: return@Function null
val imm = context.getSystemService<InputMethodManager>() ?: return@Function null
val subtype = imm.currentInputMethodSubtype ?: return@Function null
Events("onKeyboardLanguageChange")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val tag = subtype.languageTag
if (tag.isNotEmpty()) return@Function tag
Function("getCurrentKeyboardLanguage") {
return@Function getCurrentLanguage()
}
OnStartObserving {
val context = appContext.reactContext ?: return@OnStartObserving
val uri = Settings.Secure.getUriFor("selected_input_method_subtype")
subtypeObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
val language = getCurrentLanguage()
if (language != lastLanguage) {
lastLanguage = language
sendEvent("onKeyboardLanguageChange", mapOf("language" to language))
}
}
}
@Suppress("DEPRECATION")
val locale = subtype.locale
if (locale.isNotEmpty()) return@Function locale
context.contentResolver.registerContentObserver(uri, false, subtypeObserver!!)
}
return@Function null
OnStopObserving {
subtypeObserver?.let { observer ->
appContext.reactContext?.contentResolver?.unregisterContentObserver(observer)
}
subtypeObserver = null
}
}
private fun getCurrentLanguage(): String? {
val context = appContext.reactContext ?: return null
val imm = context.getSystemService<InputMethodManager>() ?: return null
val subtype = imm.currentInputMethodSubtype ?: return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val tag = subtype.languageTag
if (tag.isNotEmpty()) return tag
}
@Suppress("DEPRECATION")
val locale = subtype.locale
if (locale.isNotEmpty()) return locale
return null
}
}