I built a custom keyboard in Flutter Android and I have a problem in typing. While typing fast, keys are missing and not typed properly.
Will removing the DebouncedClickListener class fix this missing keys issue?
RewriterKeyboardService.kt
package com.arleven.rephraseplus import android.content.ActivityNotFoundException import android.inputmethodservice.InputMethodService import android.os.* import android.util.Log import android.view.* import android.widget.* import android.view.inputmethod.* import kotlinx.coroutines.* import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.LinearLayoutManager import android.app.AlertDialog import android.content.Intent import android.view.WindowManager class DebouncedClickListener( private val debounceTime: Long = 50, // Reduced to 50ms for faster typing private val onClick: () -> Unit ) : View.OnClickListener { private var lastClickTime = 0L override fun onClick(v: View?) { val now = System.currentTimeMillis() if (now - lastClickTime >= debounceTime) { lastClickTime = now onClick() } } } private const val TAG = "KeyboardService" class RewriterKeyboardService : InputMethodService() { private fun handleKeyPress(key: String) { // ✅ OK } private enum class ShiftState { OFF, ONCE, LOCKED } private var shiftState = ShiftState.OFF private var lastShiftTapTime = 0L private val DOUBLE_TAP_TIMEOUT = 400L private var fixedKeyboardHeight = 0 private lateinit var floatingBackspace: ImageButton private var previousKeyboardMode: KeyboardMode = KeyboardMode.LETTERS private enum class KeyboardMode { LETTERS, SYMBOLS_1, SYMBOLS_2, EMOJIS } private enum class EmojiCategory { RECENT, SMILEYS, ANIMALS, FOOD, ACTIVITY, OBJECTS, TRAVEL, SYMBOLS, FLAGS } private val emojiMap = mapOf( EmojiCategory.SMILEYS to listOf("😀", "😃", "😄", "😁", "😆", "😅", "😂", ), EmojiCategory.ANIMALS to listOf("🐵", "🐒", "🦍", "🦧", "🐶", "🐕", "🦮", ), EmojiCategory.FOOD to listOf("🍎", "🍏", "🍐", "🍊", "🍋", "🍌", "🍉", "🥭", "🍎", "🍏"), EmojiCategory.ACTIVITY to listOf("⚽", "🏀", "🏈", "⚾", "🥎", "🏐", "🏉", "🎲", "♟️", "🧩"), EmojiCategory.OBJECTS to listOf("📱", "📲", "☎️", "📞", "📟", "📠", "🔋", "🛏️", "🛋️", "🪑", "🚽", "🪠", "🚿", "🛁", "🪥", "🧼", "🪒", "🧽", ), EmojiCategory.TRAVEL to listOf("🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", ), EmojiCategory.SYMBOLS to listOf("❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🗄️", "🗑️", "🔒", "🔓", "🔏", "🔐", "🔑", "🗝️"), EmojiCategory.FLAGS to listOf("🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲","🇦🇴","🇦🇶", ) ) private val emojiCategories = listOf( "🕘\uFE0E", // Recent "😀\uFE0E", // Smileys "🐶\uFE0E", // Animals "🍔\uFE0E", // Food "⚽\uFE0E", // Activity "🏠\uFE0E", // Objects "🚗\uFE0E", // Travel "💡\uFE0E", // Symbols "🚩\uFE0E" // Flags ) private val recentEmojis = mutableListOf<String>() private var currentEmojiCategory = EmojiCategory.SMILEYS private var pendingText: String? = null private var selectedFeature: String? = null private lateinit var keyboardView: View private var inputConnection: InputConnection? = null private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private lateinit var purchasePreferences: PurchasePreferences override fun onCreate() { super.onCreate() purchasePreferences = PurchasePreferences(this) } private fun checkApiLimitAndProceed(action: () -> Unit) { purchasePreferences.resetIfNewDay() if (purchasePreferences.isProUser()) { // Pro user - unlimited access action() } else { val usedCalls = purchasePreferences.getDailyCallCount() if (usedCalls < 25) { // Free user - within limit purchasePreferences.incrementDailyCallCount() action() } else { // Free user - limit reached, open app's purchase screen directly openPurchaseScreen() } } } private fun openPurchaseScreen() { try { requestHideSelf(0) val intent = Intent(this, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) putExtra("SHOW_PAYWALL", true) } startActivity(intent) } catch (e: Exception) { Log.e(TAG, "Failed to open purchase screen", e) } } // ================= LIFECYCLE ================= private fun bindKeyWithDebounce(button: Button, text: String) { button.setOnClickListener(DebouncedClickListener(debounceTime = 50) { ensureInputConnection() inputConnection?.commitText(text, 1) updateFloatingBackspaceVisibility() }) } private fun bindKeyWithDebounce(buttonId: Int, text: String) { val button = keyboardView.findViewById<Button>(buttonId) button?.setOnClickListener(DebouncedClickListener(debounceTime = 50) { ensureInputConnection() inputConnection?.commitText(text, 1) updateFloatingBackspaceVisibility() }) } private fun hideAllKeyboards() { keyboardView.findViewById<View>(R.id.layout_letters)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_1)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_2)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_emojis)?.visibility = View.GONE } private val backspaceHandler = Handler(Looper.getMainLooper()) private var isBackspacePressed = false private val backspaceRunnable = object : Runnable { override fun run() { if (isBackspacePressed) { ensureInputConnection() inputConnection?.deleteSurroundingText(1, 0) backspaceHandler.postDelayed(this, 50) // speed (lower = faster) } } } private fun setupBackspace(view: View?) { view ?: return var longPressTriggered = false // Single tap → delete once view.setOnClickListener { ensureInputConnection() inputConnection?.deleteSurroundingText(1, 0) updateFloatingBackspaceVisibility() } view.setOnTouchListener { v, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { longPressTriggered = false isBackspacePressed = true backspaceHandler.postDelayed({ if (isBackspacePressed) { longPressTriggered = true backspaceRunnable.run() } }, 300) true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { isBackspacePressed = false backspaceHandler.removeCallbacks(backspaceRunnable) if (!longPressTriggered) { v.performClick() } updateFloatingBackspaceVisibility() true } else -> false } } } override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { super.onStartInputView(info, restarting) inputConnection = currentInputConnection } private fun lockKeyboardHeight() { if (fixedKeyboardHeight <= 0) return val root = keyboardView.findViewById<View>(R.id.keyboard_root) root.layoutParams = root.layoutParams.apply { height = fixedKeyboardHeight } } override fun onCreateInputView(): View { keyboardView = layoutInflater.inflate(R.layout.keyboard_view, null) keyboardView.post { if (fixedKeyboardHeight == 0) { fixedKeyboardHeight = keyboardView.height val root = keyboardView.findViewById<View>(R.id.keyboard_root) root.layoutParams = root.layoutParams.apply { height = fixedKeyboardHeight } } } keyboardView.setBackgroundColor(0xFFB9B9B9.toInt()) keyboardView.findViewById<View>(R.id.btn_close_tone)?.setOnClickListener { showKeyboardState() } bindSymbolKeys(R.id.layout_symbols_1) bindSymbolKeys(R.id.layout_symbols_2) val emojiRecycler = keyboardView.findViewById<RecyclerView>(R.id.emoji_recycler) val categoryRecycler = keyboardView.findViewById<RecyclerView>(R.id.emoji_category_recycler) val emojiAdapter = EmojiAdapter(emptyList()) { emoji -> ensureInputConnection() inputConnection?.commitText(emoji, 1) recentEmojis.remove(emoji) recentEmojis.add(0, emoji) if (recentEmojis.size > 30) recentEmojis.removeLast() updateFloatingBackspaceVisibility() // ✅ REQUIRED } emojiRecycler.layoutManager = GridLayoutManager(this, 8) emojiRecycler.adapter = emojiAdapter categoryRecycler.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) categoryRecycler.adapter = EmojiCategoryAdapter(emojiCategories) { index -> currentEmojiCategory = EmojiCategory.values()[index] updateEmojiGrid(emojiAdapter) } updateEmojiGrid(emojiAdapter) floatingBackspace = keyboardView.findViewById(R.id.floating_backspace) // Use SAME backspace logic (tap + long press) setupBackspace(floatingBackspace) // Initial state updateFloatingBackspaceVisibility() setupActionButtons() setupKeyListeners() setupSymbolSwitching() setupResultButtons() showKeyboardState() return keyboardView } private fun updateEmojiGrid(adapter: EmojiAdapter) { val list = when (currentEmojiCategory) { EmojiCategory.RECENT -> recentEmojis else -> emojiMap[currentEmojiCategory] ?: emptyList() } adapter.update(list) } private fun updateFloatingBackspaceVisibility() { // 🚫 Floating backspace ONLY for emoji keyboard if (keyboardMode != KeyboardMode.EMOJIS) { floatingBackspace.visibility = View.GONE return } ensureInputConnection() val text = inputConnection?.getSelectedText(0) ?: inputConnection?.getExtractedText( ExtractedTextRequest(), 0 )?.text floatingBackspace.visibility = if (!text.isNullOrEmpty()) View.VISIBLE else View.GONE } private fun bindSymbolKeys(containerId: Int) { val container = keyboardView.findViewById<ViewGroup>(containerId) val blockedIds = setOf( R.id.key_abc_1, R.id.key_abc_2, R.id.key_symbols, R.id.key_symbols_1, R.id.key_symbols_2, R.id.key_emoji_symbols_1, R.id.key_emoji_symbols_2, R.id.key_backspace, R.id.key_backspace_symbols_1, R.id.key_backspace_symbols_2, R.id.key_space_symbols_1, R.id.key_space_symbols_2, R.id.key_enter_symbols_1, R.id.key_enter_symbols_2 ) for (i in 0 until container.childCount) { val row = container.getChildAt(i) as? ViewGroup ?: continue for (j in 0 until row.childCount) { val btn = row.getChildAt(j) as? Button ?: continue // Skip navigation/special keys if (btn.id in blockedIds) continue if (btn.text.isNullOrBlank()) continue // Use debounced click listener bindKeyWithDebounce(btn, btn.text.toString()) } } } // ================= ACTIONS ================= private fun showSnackbar(message: String) = runOnUiThread { val container = keyboardView.findViewById<FrameLayout>(R.id.snackbar_container) val textView = container.findViewById<TextView>(R.id.snackbar_text) textView.text = message container.visibility = View.VISIBLE textView.alpha = 0f textView.translationY = 40f textView.animate() .alpha(1f) .translationY(0f) .setDuration(180) .start() Handler(Looper.getMainLooper()).postDelayed({ textView.animate() .alpha(0f) .translationY(40f) .setDuration(200) .withEndAction { container.visibility = View.GONE } .start() }, 2000) } private fun Int.dp(): Int = (this * resources.displayMetrics.density).toInt() private fun getInputTextOrShowError(actionName: String): String? { val text = inputConnection?.getSelectedText(0)?.toString() ?: inputConnection?.getExtractedText( ExtractedTextRequest(), 0 )?.text?.toString() if (text.isNullOrBlank()) { showSnackbar("Please add text to $actionName") return null } return text } private fun handleRephrase() { if (!ensureInputConnection()) return // 🔥 SAVE CURRENT MODE previousKeyboardMode = keyboardMode val text = getInputTextOrShowError("rephrase") ?: return pendingText = text // Check API limit before proceeding checkApiLimitAndProceed { selectedFeature = "change-tone" showToneState() } } fun onToneClick(view: View) { val tone = view.tag as String val text = pendingText ?: return // Note: Tone selection is already within the API call flow // so we don't need to check limits here again showLoadingState() serviceScope.launch { val result = callApiWithTone(text, tone) showResultState(result) } } // ================= API ================= private suspend fun callApiWithTone(text: String, tone: String): String = withContext(Dispatchers.IO) { try { val url = "https://rephrase-plus.onrender.com/v1/convert" + "?version=v2&mode=change-tone&tone=$tone" val conn = URL(url).openConnection() as HttpURLConnection conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true val body = JSONObject().put("text", text).toString() conn.outputStream.use { it.write(body.toByteArray()) } val response = conn.inputStream.bufferedReader().readText() JSONObject(response).optString("data", response) } catch (e: Exception) { Log.e(TAG, "API ERROR", e) "Error: ${e.message}" } } private suspend fun callApi(text: String, feature: String): String = withContext(Dispatchers.IO) { try { val url = "https://rephrase-plus.onrender.com/v1/convert" + "?version=v2&mode=$feature" val conn = URL(url).openConnection() as HttpURLConnection conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true val body = JSONObject().put("text", text).toString() conn.outputStream.use { it.write(body.toByteArray()) } val response = conn.inputStream.bufferedReader().readText() JSONObject(response).optString("data", response) } catch (e: Exception) { Log.e(TAG, "API ERROR", e) "Error: ${e.message}" } } private var keyboardMode = KeyboardMode.LETTERS private fun showLetters() { keyboardMode = KeyboardMode.LETTERS lockKeyboardHeight() shiftState = ShiftState.OFF updateShiftKeyUI() floatingBackspace.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_letters).visibility = View.VISIBLE keyboardView.findViewById<View>(R.id.layout_symbols_1).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_2).visibility = View.GONE } private fun showSymbols1() { keyboardMode = KeyboardMode.SYMBOLS_1 floatingBackspace.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_letters).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_2).visibility = View.GONE val symbols1 = keyboardView.findViewById<View>(R.id.layout_symbols_1) symbols1.visibility = View.VISIBLE bindSymbolKeys(R.id.layout_symbols_1) } private fun showSymbols2() { keyboardMode = KeyboardMode.SYMBOLS_2 floatingBackspace.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_letters).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_1).visibility = View.GONE val symbols2 = keyboardView.findViewById<View>(R.id.layout_symbols_2) symbols2.visibility = View.VISIBLE bindSymbolKeys(R.id.layout_symbols_2) // 🔥 ensure binding } private fun bindAllKeys(containerId: Int) { val container = keyboardView.findViewById<ViewGroup>(containerId) for (i in 0 until container.childCount) { val row = container.getChildAt(i) as? ViewGroup ?: continue for (j in 0 until row.childCount) { val btn = row.getChildAt(j) as? Button ?: continue // FIX: Use OnClickListener for better performance btn.setOnClickListener { ensureInputConnection() inputConnection?.commitText(btn.text.toString(), 1) } } } } // ================= UI STATES ================= private fun showKeyboardState() = runOnUiThread { keyboardView.findViewById<View>(R.id.layout_letters)?.visibility = View.VISIBLE keyboardView.findViewById<View>(R.id.tone_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.loading_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.result_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_emojis)?.visibility = View.GONE updateFloatingBackspaceVisibility() } private fun showToneState() = runOnUiThread { lockKeyboardHeight() hideAllKeyboards() keyboardView.findViewById<View>(R.id.tone_container)?.visibility = View.VISIBLE keyboardView.findViewById<View>(R.id.loading_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.result_container)?.visibility = View.GONE floatingBackspace.visibility = View.GONE } private fun showLoadingState() = runOnUiThread { lockKeyboardHeight() hideAllKeyboards() keyboardView.findViewById<View>(R.id.loading_container)?.visibility = View.VISIBLE keyboardView.findViewById<View>(R.id.tone_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.result_container)?.visibility = View.GONE floatingBackspace.visibility = View.GONE } private fun showResultState(text: String) = runOnUiThread { lockKeyboardHeight() hideAllKeyboards() keyboardView.findViewById<TextView>(R.id.result_text)?.text = text keyboardView.findViewById<View>(R.id.result_container)?.visibility = View.VISIBLE keyboardView.findViewById<View>(R.id.loading_container)?.visibility = View.GONE keyboardView.findViewById<View>(R.id.tone_container)?.visibility = View.GONE floatingBackspace.visibility = View.GONE } // ================= HELPERS ================= private fun setupActionButtons() { keyboardView.findViewById<Button>(R.id.btn_rephrase) ?.setOnClickListener { handleRephrase() } keyboardView.findViewById<Button>(R.id.btn_check_grammar) ?.setOnClickListener { handleAction("fix-grammar") } keyboardView.findViewById<Button>(R.id.btn_summarize) ?.setOnClickListener { handleAction("summarise") } keyboardView.findViewById<Button>(R.id.btn_ai_reply) ?.setOnClickListener { handleAction("ai-reply") } keyboardView.findViewById<Button>(R.id.btn_email_gen) ?.setOnClickListener { handleAction("email-generator") } keyboardView.findViewById<Button>(R.id.btn_prompt_gen) ?.setOnClickListener { handleAction("prompt-generator") } } private fun setupKeyListeners() { // Number keys with debouncing listOf("1","2","3","4","5","6","7","8","9","0").forEach { val id = resources.getIdentifier("key_$it", "id", packageName) bindKeyWithDebounce(id, it) } // Letter keys listOf( "q","w","e","r","t","y","u","i","o","p", "a","s","d","f","g","h","j","k","l", "z","x","c","v","b","n","m" ).forEach { bindLetterKey( resources.getIdentifier("key_$it", "id", packageName), it ) } // Space keys bindSpaceKey(R.id.key_space) bindSpaceKey(R.id.key_space_symbols_1) bindSpaceKey(R.id.key_space_symbols_2) // Enter keys bindEnter(R.id.key_enter) bindEnter(R.id.key_enter_symbols_1) bindEnter(R.id.key_enter_symbols_2) // Shift key keyboardView.findViewById<Button>(R.id.key_shift)?.setOnClickListener { val now = System.currentTimeMillis() shiftState = if (now - lastShiftTapTime < DOUBLE_TAP_TIMEOUT) { ShiftState.LOCKED // caps lock } else { when (shiftState) { ShiftState.OFF -> ShiftState.ONCE ShiftState.ONCE -> ShiftState.OFF ShiftState.LOCKED -> ShiftState.OFF } } lastShiftTapTime = now updateShiftKeyUI() } // Backspace setupBackspace(keyboardView.findViewById(R.id.key_backspace)) setupBackspace(keyboardView.findViewById(R.id.key_backspace_symbols_1)) setupBackspace(keyboardView.findViewById(R.id.key_backspace_symbols_2)) // Emoji keys listOf( R.id.key_emoji_letters, R.id.key_emoji_symbols_1, R.id.key_emoji_symbols_2 ).forEach { id -> keyboardView.findViewById<Button>(id)?.setOnClickListener { showEmojis() } } // Emoji keyboard → ABC keyboardView.findViewById<Button>(R.id.key_abc_emoji)?.setOnClickListener { hideEmojis() } // Emoji keyboard space keyboardView.findViewById<Button>(R.id.key_space_emoji)?.setOnClickListener { ensureInputConnection() inputConnection?.commitText(" ", 1) updateFloatingBackspaceVisibility() } // Emoji keyboard enter keyboardView.findViewById<Button>(R.id.key_enter_emoji)?.setOnClickListener { ensureInputConnection() inputConnection?.sendKeyEvent( KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER) ) updateFloatingBackspaceVisibility() } // Dot key keyboardView.findViewById<Button>(R.id.key_dot)?.setOnClickListener( DebouncedClickListener(debounceTime = 50) { ensureInputConnection() inputConnection?.commitText(".", 1) updateFloatingBackspaceVisibility() } ) } private fun updateLetterKeysUI() { val letters = listOf( "q","w","e","r","t","y","u","i","o","p", "a","s","d","f","g","h","j","k","l", "z","x","c","v","b","n","m" ) letters.forEach { letter -> val id = resources.getIdentifier("key_$letter", "id", packageName) val btn = keyboardView.findViewById<Button>(id) ?: return@forEach btn.text = when (shiftState) { ShiftState.OFF -> letter.lowercase() ShiftState.ONCE, ShiftState.LOCKED -> letter.uppercase() } } } private fun bindLetterKey(id: Int, letter: String) { val btn = keyboardView.findViewById<Button>(id) ?: return btn.setOnClickListener(DebouncedClickListener(debounceTime = 50) { ensureInputConnection() val output = when (shiftState) { ShiftState.OFF -> letter.lowercase() ShiftState.ONCE, ShiftState.LOCKED -> letter.uppercase() } inputConnection?.commitText(output, 1) updateFloatingBackspaceVisibility() // Auto-reset after single use if (shiftState == ShiftState.ONCE) { shiftState = ShiftState.OFF updateShiftKeyUI() } }) } private fun updateShiftKeyUI() { val shiftKey = keyboardView.findViewById<Button>(R.id.key_shift) // 🔒 keep color exactly the same in all states shiftKey?.alpha = 1f when (shiftState) { ShiftState.OFF -> { shiftKey?.text = "↑" // outlined arrow } ShiftState.ONCE -> { shiftKey?.text = "↑" // outlined arrow } ShiftState.LOCKED -> { shiftKey?.text = "⇪" // filled caps arrow } } updateLetterKeysUI() } private fun setupSymbolSwitching() { // !#1 → Symbols page 1 keyboardView.findViewById<Button>(R.id.key_symbols)?.setOnClickListener { showSymbols1() } // 1/2 → Symbols page 2 keyboardView.findViewById<Button>(R.id.key_symbols_2)?.setOnClickListener { showSymbols2() } // 2/2 → Symbols page 1 keyboardView.findViewById<Button>(R.id.key_symbols_1)?.setOnClickListener { showSymbols1() } // ABC → Letters keyboardView.findViewById<Button>(R.id.key_abc_1)?.setOnClickListener { showLetters() } keyboardView.findViewById<Button>(R.id.key_abc_2)?.setOnClickListener { showLetters() } } private fun setupResultButtons() { keyboardView.findViewById<Button>(R.id.btn_apply)?.setOnClickListener { inputConnection?.commitText( keyboardView.findViewById<TextView>(R.id.result_text).text, 1 ) showKeyboardState() } keyboardView.findViewById<Button>(R.id.btn_close) ?.setOnClickListener { showKeyboardState() } } private fun handleAction(feature: String) { if (!ensureInputConnection()) return val actionName = when (feature) { "fix-grammar" -> "check grammar" "summarise" -> "summarize" "ai-reply" -> "generate AI reply" "email-generator" -> "generate email" "prompt-generator" -> "generate prompt" else -> "process text" } val text = getInputTextOrShowError(actionName) ?: return // Check API limit before proceeding checkApiLimitAndProceed { showLoadingState() serviceScope.launch { val result = callApi(text, feature) showResultState(result) } } } private fun ensureInputConnection(): Boolean { if (inputConnection == null) inputConnection = currentInputConnection return inputConnection != null } private fun runOnUiThread(action: () -> Unit) { Handler(Looper.getMainLooper()).post(action) } override fun onEvaluateFullscreenMode(): Boolean = false override fun onDestroy() { serviceScope.cancel() super.onDestroy() } private fun showEmojis() { keyboardMode = KeyboardMode.EMOJIS lockKeyboardHeight() keyboardView.findViewById<View>(R.id.layout_letters).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_1).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_symbols_2).visibility = View.GONE keyboardView.findViewById<View>(R.id.layout_emojis).visibility = View.VISIBLE floatingBackspace.bringToFront() updateFloatingBackspaceVisibility() } private fun hideEmojis() { showLetters() keyboardView.findViewById<View>(R.id.layout_emojis).visibility = View.GONE // 🚫 HARD STOP floatingBackspace.visibility = View.GONE } private fun bindSpaceKey(id: Int) { keyboardView.findViewById<View>(id)?.setOnClickListener( DebouncedClickListener(debounceTime = 50) { ensureInputConnection() inputConnection?.commitText(" ", 1) updateFloatingBackspaceVisibility() } ) } private fun bindEnter(id: Int) { keyboardView.findViewById<Button>(id)?.setOnClickListener( DebouncedClickListener(debounceTime = 50) { ensureInputConnection() inputConnection?.sendKeyEvent( KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER) ) updateFloatingBackspaceVisibility()