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 @@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Persistent personal dictionary with in-app word management, shared by all correction modes.
- Global reset-to-default control restoring the bundled dictionary, Grammalecte rule defaults and clearing the personal dictionary.
- Versioned JSON import/export for non-sensitive correction preferences, using Android's system document picker without storage permissions; the personal dictionary is intentionally excluded.
- Dedicated correction status screen showing the native Android spell-checker and Grammalecte IME activation/selection state, with direct access to the relevant system settings.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Exit criterion: correction remains usable in common editable applications even w
- [x] Personal dictionary
- [x] Reset-to-default controls
- [x] Import/export of non-sensitive preferences
- [ ] Clear status screen for native spell-checker and IME activation
- [x] Clear status screen for native spell-checker and IME activation

## Phase 4 — Product polish

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fr.grammalecteandroid.unofficial

import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertFalse
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class CorrectionStatusManifestTest {
@Suppress("DEPRECATION")
@Test
fun correctionStatusActivityIsInternal() {
val context =
ApplicationProvider.getApplicationContext<Context>()

val activity =
context.packageManager.getActivityInfo(
ComponentName(
context,
CorrectionStatusActivity::class.java,
),
PackageManager.GET_META_DATA,
)

assertFalse(
"Correction status activity must not be exported",
activity.exported,
)
}
}
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
android:name=".PersonalDictionaryActivity"
android:exported="false" />

<activity
android:name=".CorrectionStatusActivity"
android:exported="false" />

<activity
android:name=".MainActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package fr.grammalecteandroid.unofficial

import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.view.inputmethod.InputMethodManager
import android.view.textservice.SentenceSuggestionsInfo
import android.view.textservice.SpellCheckerSession
import android.view.textservice.SuggestionsInfo
import android.view.textservice.TextServicesManager
import java.util.Locale

data class CorrectionStatus(
val spellChecker: SpellCheckerStatus,
val ime: ImeStatus,
)

enum class SpellCheckerStatus {
SELECTED,
NOT_SELECTED,
DISABLED,
UNAVAILABLE,
}

enum class ImeStatus {
SELECTED,
ENABLED,
DISABLED,
}

class CorrectionStatusReader(
private val context: Context,
) {
fun read(): CorrectionStatus =
CorrectionStatus(
spellChecker = readSpellCheckerStatus(),
ime = readImeStatus(),
)

private fun readSpellCheckerStatus(): SpellCheckerStatus {
val manager =
context.getSystemService(
TextServicesManager::class.java,
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!manager.isSpellCheckerEnabled) {
return SpellCheckerStatus.DISABLED
}

return if (
manager.currentSpellCheckerInfo
?.packageName == context.packageName
) {
SpellCheckerStatus.SELECTED
} else {
SpellCheckerStatus.NOT_SELECTED
}
}

val session =
manager.newSpellCheckerSession(
null,
Locale.FRANCE,
EMPTY_SPELL_CHECKER_LISTENER,
false,
)

return try {
when {
session == null ->
SpellCheckerStatus.UNAVAILABLE

session.spellChecker.packageName ==
context.packageName ->
SpellCheckerStatus.SELECTED

else ->
SpellCheckerStatus.NOT_SELECTED
}
} finally {
session?.close()
}
}

private fun readImeStatus(): ImeStatus {
val manager =
context.getSystemService(
InputMethodManager::class.java,
)

val component =
ComponentName(
context,
GrammalecteImeService::class.java,
)

val enabled =
manager.enabledInputMethodList
.any { inputMethod ->
ComponentName(
inputMethod.packageName,
inputMethod.serviceName,
) == component
}

val selectedComponent =
Settings.Secure
.getString(
context.contentResolver,
Settings.Secure.DEFAULT_INPUT_METHOD,
)?.let(
ComponentName::unflattenFromString,
)

return when {
selectedComponent == component ->
ImeStatus.SELECTED

enabled ->
ImeStatus.ENABLED

else ->
ImeStatus.DISABLED
}
}

private companion object {
val EMPTY_SPELL_CHECKER_LISTENER =
object :
SpellCheckerSession.SpellCheckerSessionListener {
override fun onGetSuggestions(results: Array<SuggestionsInfo>?) = Unit

override fun onGetSentenceSuggestions(results: Array<SentenceSuggestionsInfo>?) = Unit
}
}
}
Loading
Loading