From 5f724dd17c3818b1d3930f248233995bb7966444 Mon Sep 17 00:00:00 2001 From: zefutoff Date: Mon, 5 Oct 2026 22:04:04 +0200 Subject: [PATCH] fix: refresh IME analysis on selection changes --- CHANGELOG.md | 1 + .../ImeInputConnectionEndToEndTest.kt | 23 +++++++- .../unofficial/GrammalecteImeService.kt | 56 +++++++++++++++++++ docs/testing.md | 1 + 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd73bbb..121a9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Grammalecte vendoring is normalized and checked for reproducible output. - CI verifies the permissions of the final debug APK in addition to source manifests. - IME analysis now uses a single serialized background worker so QuickJS engine creation, evaluation and shutdown cannot race during rapid repeated corrections or service destruction. +- The IME now reacts to selection changes while it is already open, allowing text selected after switching keyboards to be analyzed automatically. ### Fixed diff --git a/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/ImeInputConnectionEndToEndTest.kt b/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/ImeInputConnectionEndToEndTest.kt index f7cd636..4850994 100644 --- a/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/ImeInputConnectionEndToEndTest.kt +++ b/app/src/androidTest/kotlin/fr/grammalecteandroid/unofficial/ImeInputConnectionEndToEndTest.kt @@ -17,7 +17,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ImeInputConnectionEndToEndTest { @Test - fun imeReplacesSelectedTextThroughRealInputConnection() { + fun imeRefreshesLateSelectionAndReplacesItThroughRealInputConnection() { val instrumentation = InstrumentationRegistry.getInstrumentation() @@ -70,7 +70,6 @@ class ImeInputConnectionEndToEndTest { activity.editor.requestFocus() activity.editor.setSelection( - 0, INITIAL_TEXT.length, ) @@ -122,13 +121,28 @@ class ImeInputConnectionEndToEndTest { SystemClock.sleep(IME_SWITCH_DELAY_MILLIS) + requireNotNull( + waitForNode( + NO_SELECTION_LABEL, + ), + ) { + "Expected Grammalecte IME to start without a selection" + } + + scenario.onActivity { activity -> + activity.editor.setSelection( + 0, + INITIAL_TEXT.length, + ) + } + val button = requireNotNull( waitForNode( CORRECT_AND_RETURN_LABEL, ), ) { - "Expected Grammalecte IME correction action" + "Expected Grammalecte IME to refresh the late selection" } assertTrue( @@ -324,6 +338,9 @@ class ImeInputConnectionEndToEndTest { const val EXPECTED_TEXT = "Je suis allé au magasin hier." + const val NO_SELECTION_LABEL = + "Aucune sélection." + const val CORRECT_AND_RETURN_LABEL = "Corriger tout et revenir" diff --git a/app/src/main/kotlin/fr/grammalecteandroid/unofficial/GrammalecteImeService.kt b/app/src/main/kotlin/fr/grammalecteandroid/unofficial/GrammalecteImeService.kt index 7bc9688..9300473 100644 --- a/app/src/main/kotlin/fr/grammalecteandroid/unofficial/GrammalecteImeService.kt +++ b/app/src/main/kotlin/fr/grammalecteandroid/unofficial/GrammalecteImeService.kt @@ -21,6 +21,14 @@ import java.util.concurrent.Executors class GrammalecteImeService : InputMethodService() { private val mainHandler = Handler(Looper.getMainLooper()) + + private val selectionRefreshRunnable = + Runnable { + if (::selectedTextView.isInitialized) { + loadSelectionAndAnalyze() + } + } + private val analysisExecutor = Executors.newSingleThreadExecutor { runnable -> Thread(runnable, "GrammalecteImeAnalysis") @@ -294,9 +302,47 @@ class GrammalecteImeService : InputMethodService() { restarting, ) + mainHandler.removeCallbacks( + selectionRefreshRunnable, + ) + loadSelectionAndAnalyze() } + override fun onUpdateSelection( + oldSelStart: Int, + oldSelEnd: Int, + newSelStart: Int, + newSelEnd: Int, + candidatesStart: Int, + candidatesEnd: Int, + ) { + super.onUpdateSelection( + oldSelStart, + oldSelEnd, + newSelStart, + newSelEnd, + candidatesStart, + candidatesEnd, + ) + + if ( + oldSelStart == newSelStart && + oldSelEnd == newSelEnd + ) { + return + } + + mainHandler.removeCallbacks( + selectionRefreshRunnable, + ) + + mainHandler.postDelayed( + selectionRefreshRunnable, + SELECTION_REFRESH_DELAY_MILLIS, + ) + } + private fun loadSelectionAndAnalyze() { val selectedText = currentInputConnection @@ -305,6 +351,8 @@ class GrammalecteImeService : InputMethodService() { .orEmpty() if (selectedText.isEmpty()) { + analysisGeneration++ + originalSelectedText = "" workingText = "" @@ -767,6 +815,10 @@ class GrammalecteImeService : InputMethodService() { } override fun onDestroy() { + mainHandler.removeCallbacks( + selectionRefreshRunnable, + ) + analysisGeneration++ analysisExecutor.execute { @@ -810,6 +862,10 @@ class GrammalecteImeService : InputMethodService() { ) } + private companion object { + const val SELECTION_REFRESH_DELAY_MILLIS = 150L + } + private fun weightedButtonParams(): LinearLayout.LayoutParams = LinearLayout.LayoutParams( 0, diff --git a/docs/testing.md b/docs/testing.md index be8823a..e6bc995 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -108,6 +108,7 @@ Current application-level instrumentation verifies that: - the IME exposes the expected `android.view.im` metadata; - the IME can be enabled and selected through Android's input-method framework; - a real editable `EditText` exposes selected text to the IME through a real `InputConnection`; +- a selection made after the IME is already open triggers a fresh Grammalecte analysis; - the Grammalecte IME can analyze that selection and commit corrected text back into the real editor; - the `ACTION_PROCESS_TEXT` activity is discoverable for `text/plain`; - a real Android `TextServicesManager` session reaches the packaged `SpellCheckerService` and returns a Grammalecte correction end to end.