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 @@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Local and CI checks now share the same `make check` and `make assemble` entry points.
- 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.

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Exit criterion: an installable APK performs local French spelling and grammar co
- [x] Validate clean replacement in SMS
- [x] Add a basic user-friendly IME activation and selection flow
- [x] Add an easy way to return to the previous keyboard
- [ ] Review QuickJS execution serialization inside the IME
- [x] Review QuickJS execution serialization inside the IME
- [ ] Add automated IME integration tests where practical
- [x] Cover IME discovery, permission and metadata
- [ ] Cover IME lifecycle and selected-text replacement through `InputConnection`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ import android.widget.ScrollView
import android.widget.TextView
import fr.grammalecteandroid.core.GrammarIssue
import fr.grammalecteandroid.engine.GrammalecteQuickJsEngine
import java.util.concurrent.Executors

class GrammalecteImeService : InputMethodService() {
private val mainHandler = Handler(Looper.getMainLooper())
private val analysisExecutor =
Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "GrammalecteImeAnalysis")
}
private lateinit var selectedTextView: TextView
private lateinit var statusView: TextView
private lateinit var issuesLayout: LinearLayout
Expand All @@ -32,7 +37,10 @@ class GrammalecteImeService : InputMethodService() {
private var originalSelectedText = ""
private var latestIssues: List<GrammarIssue> = emptyList()

// The engine is created, used and closed only on analysisExecutor.
private var engine: GrammalecteQuickJsEngine? = null

@Volatile
private var analysisGeneration = 0

override fun onCreateInputView(): View {
Expand Down Expand Up @@ -335,7 +343,11 @@ class GrammalecteImeService : InputMethodService() {
latestIssues = emptyList()
setApplyAllButtonsEnabled(false)

Thread {
analysisExecutor.execute {
if (generation != analysisGeneration) {
return@execute
}

val result =
runCatching {
val currentEngine =
Expand Down Expand Up @@ -370,7 +382,7 @@ class GrammalecteImeService : InputMethodService() {
setApplyButtonsEnabled(false)
}
}
}.start()
}
}

private fun renderIssues(
Expand Down Expand Up @@ -757,8 +769,11 @@ class GrammalecteImeService : InputMethodService() {
override fun onDestroy() {
analysisGeneration++

engine?.close()
engine = null
analysisExecutor.execute {
engine?.close()
engine = null
}
analysisExecutor.shutdown()

super.onDestroy()
}
Expand Down
Loading
Loading