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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 9 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
}

buildFeatures {
Expand All @@ -109,6 +109,12 @@ android {
compose = true
}

externalNativeBuild {
ndkBuild {
path = file("src/main/jni/Android.mk")
}
}

signingConfigs {
create("release") {
if (keystorePropertiesFile.exists()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
@file:Suppress("ktlint", "detekt.all")

package be.scri.helpers

Expand Down
37 changes: 33 additions & 4 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import be.scri.helpers.DatabaseManagers
import be.scri.helpers.EmojiUtils.insertEmoji
import be.scri.helpers.KeyboardBase
import be.scri.helpers.LanguageMappingConstants.getLanguageAlias
import be.scri.helpers.NativeSuggestionEngine
import be.scri.helpers.PreferencesHelper
import be.scri.helpers.PreferencesHelper.getHoldKeyStyle
import be.scri.helpers.PreferencesHelper.getIsDarkModeOrNot
Expand Down Expand Up @@ -123,6 +124,7 @@ abstract class GeneralKeyboardIME(
private val shiftPermToggleSpeed: Int = DEFAULT_SHIFT_PERM_TOGGLE_SPEED

private lateinit var dbManagers: DatabaseManagers
private lateinit var nativeSuggestionEngine: NativeSuggestionEngine
internal lateinit var suggestionHandler: SuggestionHandler
internal lateinit var autocompletionHandler: AutocompletionHandler
private lateinit var autocompletionManager: AutocompletionDataManager
Expand Down Expand Up @@ -205,11 +207,19 @@ abstract class GeneralKeyboardIME(
override fun onCreate() {
super.onCreate()
dbManagers = DatabaseManagers(this)
nativeSuggestionEngine = NativeSuggestionEngine(this)
suggestionHandler = SuggestionHandler(this)
autocompletionManager = dbManagers.autocompletionManager
autocompletionHandler = AutocompletionHandler(this)
}

override fun onDestroy() {
if (this::nativeSuggestionEngine.isInitialized) {
nativeSuggestionEngine.close()
}
super.onDestroy()
}

/**
* Creates the main view for the input method, inflating it from XML and setting up the keyboard.
*
Expand Down Expand Up @@ -1031,8 +1041,14 @@ abstract class GeneralKeyboardIME(
fun getAutocompletions(
prefix: String,
limit: Int = 3,
): List<String> =
try {
): List<String> {
if (this::nativeSuggestionEngine.isInitialized) {
val nativeCompletions = nativeSuggestionEngine.getAutocompletions(language, prefix, limit)
if (nativeCompletions.isNotEmpty()) {
return nativeCompletions
}
}
return try {
dbManagers.autocompletionManager.getAutocompletions(prefix, limit)
} catch (e: SQLiteException) {
Log.e("GeneralKeyboardIME", "Database error in autocompletion", e)
Expand All @@ -1041,6 +1057,7 @@ abstract class GeneralKeyboardIME(
Log.e("GeneralKeyboardIME", "Illegal state in autocompletion", e)
emptyList()
}
}

/**
* Gets the current text in the command bar without the cursor.
Expand Down Expand Up @@ -1309,7 +1326,16 @@ abstract class GeneralKeyboardIME(
fun getNextWordSuggestions(
wordSuggestions: HashMap<String, List<String>>,
lastWord: String?,
): List<String>? = lastWord?.let { wordSuggestions[it.lowercase()] }
): List<String>? {
if (lastWord == null) return null
if (this::nativeSuggestionEngine.isInitialized) {
val nativeSuggestions = nativeSuggestionEngine.getNextWordSuggestions(language, lastWord)
if (nativeSuggestions.isNotEmpty()) {
return nativeSuggestions
}
}
return wordSuggestions[lastWord.lowercase()]
}

/**
* Finds the required grammatical case(s) for a preposition.
Expand Down Expand Up @@ -1687,7 +1713,10 @@ abstract class GeneralKeyboardIME(
hasLinguisticSuggestions && emojiCount == 0 -> {
setSuggestionButton(uiManager.pluralBtn!!, suggestion2)
}

!hasLinguisticSuggestions && emojiCount != 0 -> {
setSuggestionButton(uiManager.binding.translateBtn, suggestion2)
uiManager.updateButtonVisibility(currentState, true, autoSuggestEmojis)
}
else -> {
setSuggestionButton(uiManager.binding.translateBtn, suggestion2)
setSuggestionButton(uiManager.pluralBtn!!, suggestion3)
Expand Down
Binary file added app/src/main/assets/dicts/main_bg.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_bn.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_de.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_el.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_en-GB.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_en-US.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_es.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_fr.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_hu.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_it.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_nl.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_pl.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_pt-BR.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_pt-PT.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_ro.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_ru.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_sv.dict
Binary file not shown.
Binary file added app/src/main/assets/dicts/main_tr.dict
Binary file not shown.
191 changes: 191 additions & 0 deletions app/src/main/java/be/scri/helpers/NativeSuggestionEngine.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: GPL-3.0-or-later
@file:Suppress("ktlint", "detekt.all")

package be.scri.helpers

import android.content.Context
import android.util.Log
import be.scri.inputmethod.keyboard.ProximityInfo
import be.scri.latin.NgramContext
import be.scri.latin.common.ComposedData
import be.scri.latin.dictionary.ReadOnlyBinaryDictionary
import be.scri.latin.settings.SettingsValuesForSuggestion
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale

/**
* Handles offloading autocompletion and word suggestions to the native C++ HeliBoard
* dictionary engine compiled via Android NDK (libjni_latinime.so).
*/
class NativeSuggestionEngine(private val context: Context) {

companion object {
private const val TAG = "NativeSuggestionEngine"
private const val DICT_DIR = "dicts"
}

private val loadedDicts = HashMap<String, ReadOnlyBinaryDictionary>()
private val dummyProximityInfo = ProximityInfo()

/**
* Map a language string to its corresponding main dictionary asset name and Locale.
*/
private fun getDictInfo(language: String): Pair<String, Locale>? {
return when (language.lowercase(Locale.ROOT)) {
"english" -> Pair("main_en-US.dict", Locale.US)
"german" -> Pair("main_de.dict", Locale.GERMAN)
"spanish" -> Pair("main_es.dict", Locale("es"))
"french" -> Pair("main_fr.dict", Locale.FRENCH)
"italian" -> Pair("main_it.dict", Locale.ITALIAN)
"portuguese" -> Pair("main_pt-BR.dict", Locale("pt"))
"russian" -> Pair("main_ru.dict", Locale("ru"))
"swedish" -> Pair("main_sv.dict", Locale("sv"))
else -> null
}
}

/**
* Extracts a dictionary file from the assets to internal storage if not already extracted.
*/
private fun getOrExtractDictFile(assetName: String): File? {
val dictsFolder = File(context.filesDir, DICT_DIR)
if (!dictsFolder.exists() && !dictsFolder.mkdirs()) {
Log.e(TAG, "Failed to create dicts directory")
return null
}

val targetFile = File(dictsFolder, assetName)
if (targetFile.exists() && targetFile.length() > 0) {
return targetFile
}

try {
context.assets.open("dicts/$assetName").use { inputStream ->
FileOutputStream(targetFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
Log.i(TAG, "Successfully extracted native dictionary: $assetName")
return targetFile
} catch (e: IOException) {
Log.e(TAG, "Error extracting native dictionary $assetName from assets", e)
return null
}
}

/**
* Retrieves or loads the BinaryDictionary for the given language.
*/
@Synchronized
fun getDictionary(language: String): ReadOnlyBinaryDictionary? {
val cacheKey = language.lowercase(Locale.ROOT)
loadedDicts[cacheKey]?.let { return it }

val (assetName, locale) = getDictInfo(language) ?: return null
val dictFile = getOrExtractDictFile(assetName) ?: return null

return try {
val dict = ReadOnlyBinaryDictionary(
dictFile.absolutePath,
0L,
dictFile.length(),
false, // useFullEditDistance
locale,
"main"
)
if (dict.isValidDictionary) {
loadedDicts[cacheKey] = dict
Log.i(TAG, "Successfully loaded native dictionary for $language")
dict
} else {
Log.e(TAG, "Loaded dictionary for $language is invalid")
dict.close()
null
}
} catch (e: Exception) {
Log.e(TAG, "Error initializing ReadOnlyBinaryDictionary for $language", e)
null
}
}

/**
* Queries the native dictionary engine for autocomplete suggestions given a typed prefix.
*/
fun getAutocompletions(
language: String,
prefix: String,
limit: Int = 3
): List<String> {
val dict = getDictionary(language) ?: return emptyList()
if (prefix.isBlank()) return emptyList()

return try {
val composedData = ComposedData.createForWord(prefix)
val suggestions = dict.getSuggestions(
composedData,
NgramContext.EMPTY_PREV_WORDS_INFO,
dummyProximityInfo.nativeProximityInfo, // proximityInfoHandle
SettingsValuesForSuggestion(false, false),
1, // sessionId
1.0f, // weightForLocale
null // inOutWeightOfLangModelVsSpatialModel
)

suggestions?.map { it.mWord }
?.filter { it.isNotBlank() && it.lowercase(Locale.ROOT) != prefix.lowercase(Locale.ROOT) }
?.take(limit)
?: emptyList()
} catch (e: Exception) {
Log.e(TAG, "Error fetching native suggestions for $prefix", e)
emptyList()
}
}

/**
* Queries the native dictionary engine for next-word suggestions (bigram/trigram predictions) given the last typed word.
*/
fun getNextWordSuggestions(
language: String,
lastWord: String?,
limit: Int = 3
): List<String> {
val dict = getDictionary(language) ?: return emptyList()
if (lastWord.isNullOrBlank()) return emptyList()

return try {
val wordInfo = NgramContext.WordInfo(lastWord)
val ngramContext = NgramContext(wordInfo)
val composedData = ComposedData.createForWord("")
val suggestions = dict.getSuggestions(
composedData,
ngramContext,
dummyProximityInfo.nativeProximityInfo, // proximityInfoHandle
SettingsValuesForSuggestion(false, false),
1, // sessionId
1.0f, // weightForLocale
null // inOutWeightOfLangModelVsSpatialModel
)

suggestions?.map { it.mWord }
?.filter { it.isNotBlank() }
?.take(limit)
?: emptyList()
} catch (e: Exception) {
Log.e(TAG, "Error fetching native next-word suggestions for $lastWord", e)
emptyList()
}
}

/**
* Closes and clears all loaded dictionaries.
*/
@Synchronized
fun close() {
for (dict in loadedDicts.values) {
dict.close()
}
loadedDicts.clear()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
@file:Suppress("ktlint", "detekt.all")

package be.scri.helpers.data

Expand Down
70 changes: 70 additions & 0 deletions app/src/main/java/be/scri/inputmethod/keyboard/ProximityInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2011 The Android Open Source Project
* modified
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
*/

package be.scri.inputmethod.keyboard;

import be.scri.latin.utils.JniUtils;

public class ProximityInfo {
private static final String TAG = ProximityInfo.class.getSimpleName();
public static final int MAX_PROXIMITY_CHARS_SIZE = 16;

private long mNativeProximityInfo;
static {
JniUtils.loadNativeLibrary();
}

private static native long setProximityInfoNative(int displayWidth, int displayHeight,
int gridWidth, int gridHeight, int mostCommonKeyWidth, int mostCommonKeyHeight,
int[] proximityCharsArray, int keyCount, int[] keyXCoordinates, int[] keyYCoordinates,
int[] keyWidths, int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs,
float[] sweetSpotCenterYs, float[] sweetSpotRadii);

private static native void releaseProximityInfoNative(long nativeProximityInfo);

public ProximityInfo() {
final int gridWidth = 1;
final int gridHeight = 1;
final int[] proximityCharsArray = new int[gridWidth * gridHeight * MAX_PROXIMITY_CHARS_SIZE];
final int[] keyXCoordinates = new int[0];
final int[] keyYCoordinates = new int[0];
final int[] keyWidths = new int[0];
final int[] keyHeights = new int[0];
final int[] keyCharCodes = new int[0];
final float[] sweetSpotCenterXs = new float[0];
final float[] sweetSpotCenterYs = new float[0];
final float[] sweetSpotRadii = new float[0];

mNativeProximityInfo = setProximityInfoNative(
480, 800, // displayWidth, displayHeight
gridWidth, gridHeight,
48, 48, // mostCommonKeyWidth, mostCommonKeyHeight
proximityCharsArray,
0, // keyCount
keyXCoordinates, keyYCoordinates,
keyWidths, keyHeights,
keyCharCodes,
sweetSpotCenterXs, sweetSpotCenterYs,
sweetSpotRadii
);
}

public long getNativeProximityInfo() {
return mNativeProximityInfo;
}

@Override
protected void finalize() throws Throwable {
try {
if (mNativeProximityInfo != 0) {
releaseProximityInfoNative(mNativeProximityInfo);
mNativeProximityInfo = 0;
}
} finally {
super.finalize();
}
}
}
Loading
Loading