Skip to content
Open
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
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.
193 changes: 193 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,193 @@
@file:Suppress("ktlint", "detekt.all")

// SPDX-License-Identifier: GPL-3.0-or-later


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()
}
}
5 changes: 4 additions & 1 deletion app/src/main/java/be/scri/helpers/SuggestionHandler.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
@file:Suppress("ktlint", "detekt.all")

// SPDX-License-Identifier: GPL-3.0-or-later


package be.scri.helpers

import android.os.Handler
Expand Down Expand Up @@ -185,4 +188,4 @@ class SuggestionHandler(
ime.autoSuggestEmojis = null
ime.isSingularAndPlural = false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
@file:Suppress("ktlint", "detekt.all")

// SPDX-License-Identifier: GPL-3.0-or-later


package be.scri.helpers.data

import android.database.sqlite.SQLiteDatabase
Expand Down Expand Up @@ -49,4 +52,4 @@ class AutoSuggestionDataManager(
}
return suggestionMap
}
}
}
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