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 package expo.modules.keyboardlanguage
import android.database.ContentObserver
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.modules.ModuleDefinition
class ExpoKeyboardLanguageModule : Module() { class ExpoKeyboardLanguageModule : Module() {
private var subtypeObserver: ContentObserver? = null
private var lastLanguage: String? = null
override fun definition() = override fun definition() =
ModuleDefinition { ModuleDefinition {
Name("ExpoKeyboardLanguage") Name("ExpoKeyboardLanguage")
Events("onKeyboardLanguageChange")
Function("getCurrentKeyboardLanguage") { Function("getCurrentKeyboardLanguage") {
val context = appContext.reactContext ?: return@Function null return@Function getCurrentLanguage()
val imm = context.getSystemService<InputMethodManager>() ?: return@Function null }
val subtype = imm.currentInputMethodSubtype ?: return@Function null
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))
}
}
}
context.contentResolver.registerContentObserver(uri, false, subtypeObserver!!)
}
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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val tag = subtype.languageTag val tag = subtype.languageTag
if (tag.isNotEmpty()) return@Function tag if (tag.isNotEmpty()) return tag
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val locale = subtype.locale val locale = subtype.locale
if (locale.isNotEmpty()) return@Function locale if (locale.isNotEmpty()) return locale
return@Function null return null
}
} }
} }