Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ImeInputConnectionEndToEndTest {
@Test
fun imeReplacesSelectedTextThroughRealInputConnection() {
fun imeRefreshesLateSelectionAndReplacesItThroughRealInputConnection() {
val instrumentation =
InstrumentationRegistry.getInstrumentation()

Expand Down Expand Up @@ -70,7 +70,6 @@ class ImeInputConnectionEndToEndTest {
activity.editor.requestFocus()

activity.editor.setSelection(
0,
INITIAL_TEXT.length,
)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -305,6 +351,8 @@ class GrammalecteImeService : InputMethodService() {
.orEmpty()

if (selectedText.isEmpty()) {
analysisGeneration++

originalSelectedText = ""
workingText = ""

Expand Down Expand Up @@ -767,6 +815,10 @@ class GrammalecteImeService : InputMethodService() {
}

override fun onDestroy() {
mainHandler.removeCallbacks(
selectionRefreshRunnable,
)

analysisGeneration++

analysisExecutor.execute {
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading