1
0
Fork 0

the search for language files is now 4 times faster, resulting in faster initial start up (or faster opening the Settings screen)

This commit is contained in:
sspanak 2025-02-16 16:39:47 +02:00 committed by Dimo Karaivanov
parent ae619e1f0f
commit c844db1fa1
7 changed files with 180 additions and 157 deletions

View file

@ -29,7 +29,9 @@ jobs:
- name: Validate Dictionaries - name: Validate Dictionaries
run: ./gradlew validateLanguages run: ./gradlew validateLanguages
- name: Build Languages - name: Build Languages
run: ./gradlew copyDefinitions convertHelp buildDictionaryDownloads copyDownloadsToAssets run: ./gradlew buildDefinition buildDictionaryDownloads
- name: Copy Downloads
run: ./gradlew copyDownloadsToAssets
- name: Lint - name: Lint
run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks
- name: Build all APK variants - name: Build all APK variants

View file

@ -0,0 +1,22 @@
ext.mergeDefinitions = { String definitionsInputDir, String definitionsOutputPath ->
def merged = new File(definitionsOutputPath)
merged.delete()
boolean isFirst = true
fileTree(dir: definitionsInputDir).getFiles().each { file ->
if (!file.isFile() || !file.name.endsWith(".yml")) {
return
}
if (isFirst) {
isFirst = false
} else {
merged << "\n---\n"
}
merged << file.text
.replaceAll("\\s*#[^\n]+", "")
.replaceAll("^[ ]+\n", "")
.trim()
}
}

View file

@ -3,19 +3,13 @@ plugins {
} }
apply from: 'constants.gradle' apply from: 'constants.gradle'
apply from: 'build-dictionary.gradle' apply from: 'build-definitions.gradle'
apply from: 'build-dictionaries.gradle'
apply from: 'validate-languages.gradle' apply from: 'validate-languages.gradle'
apply from: 'help-tools.gradle' apply from: 'help-tools.gradle'
apply from: 'version-tools.gradle' apply from: 'version-tools.gradle'
tasks.register('copyDefinitions', Copy) {
from LANGUAGES_INPUT_DIR
include '**/*.yml'
into LANGUAGES_OUTPUT_DIR
}
tasks.register('validateLanguages') { tasks.register('validateLanguages') {
inputs.dir LANGUAGES_INPUT_DIR inputs.dir LANGUAGES_INPUT_DIR
outputs.dir LANGUAGE_VALIDATION_DIR outputs.dir LANGUAGE_VALIDATION_DIR
@ -26,8 +20,18 @@ tasks.register('validateLanguages') {
} }
tasks.register('buildDefinition') {
inputs.dir DEFINITIONS_INPUT_DIR
outputs.file DEFINITIONS_OUTPUT_FILE
doLast {
mergeDefinitions(DEFINITIONS_INPUT_DIR, DEFINITIONS_OUTPUT_FILE)
}
}
tasks.register('buildDictionaryDownloads') { tasks.register('buildDictionaryDownloads') {
inputs.dir DICTIONARIES_INPUT_DIR inputs.dir LANGUAGES_INPUT_DIR
outputs.dir DICTIONARIES_DOWNLOAD_DIR outputs.dir DICTIONARIES_DOWNLOAD_DIR
outputs.dir DICTIONARY_META_OUTPUT_DIR outputs.dir DICTIONARY_META_OUTPUT_DIR
@ -139,7 +143,7 @@ android {
].each { taskName -> ].each { taskName ->
try { try {
tasks.named(taskName)?.configure { tasks.named(taskName)?.configure {
dependsOn(copyDefinitions, convertHelp, validateLanguages, buildDictionaryDownloads) dependsOn(buildDefinition, convertHelp, validateLanguages, buildDictionaryDownloads)
} }
if (taskName.toLowerCase().contains("full")) { if (taskName.toLowerCase().contains("full")) {

View file

@ -21,7 +21,7 @@ ext.DICTIONARIES_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DICTIONARIES_DIR_NAME}"
ext.DICTIONARIES_DOWNLOAD_DIR = "${project.rootDir}/${DICTIONARIES_DOWNLOAD_DIR_NAME}" ext.DICTIONARIES_DOWNLOAD_DIR = "${project.rootDir}/${DICTIONARIES_DOWNLOAD_DIR_NAME}"
ext.LANGUAGES_OUTPUT_DIR = "${MAIN_ASSETS_DIR}/${LANGUAGES_DIR_NAME}" ext.LANGUAGES_OUTPUT_DIR = "${MAIN_ASSETS_DIR}/${LANGUAGES_DIR_NAME}"
ext.DEFINITIONS_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DEFINITIONS_DIR_NAME}" ext.DEFINITIONS_OUTPUT_FILE = "${LANGUAGES_OUTPUT_DIR}/${DEFINITIONS_DIR_NAME}.yml"
ext.DICTIONARY_META_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}" ext.DICTIONARY_META_OUTPUT_DIR = "${LANGUAGES_OUTPUT_DIR}/${DICTIONARIES_DIR_NAME}"
ext.DICTIONARIES_OUTPUT_DIR = "${FULL_VERSION_ASSETS_DIR}/${LANGUAGES_DIR_NAME}/${DICTIONARIES_DIR_NAME}" ext.DICTIONARIES_OUTPUT_DIR = "${FULL_VERSION_ASSETS_DIR}/${LANGUAGES_DIR_NAME}/${DICTIONARIES_DIR_NAME}"

View file

@ -19,12 +19,12 @@ public class LanguageCollection {
private final HashMap<Integer, NaturalLanguage> languages = new HashMap<>(); private final HashMap<Integer, NaturalLanguage> languages = new HashMap<>();
private LanguageCollection(Context context) { private LanguageCollection(Context context) {
for (String file : LanguageDefinition.getAllFiles(context.getAssets())) { for (LanguageDefinition definition : LanguageDefinition.getAll(context.getAssets())) {
try { try {
NaturalLanguage lang = NaturalLanguage.fromDefinition(LanguageDefinition.fromFile(context.getAssets(), file)); NaturalLanguage lang = NaturalLanguage.fromDefinition(definition);
languages.put(lang.getId(), lang); languages.put(lang.getId(), lang);
} catch (Exception e) { } catch (Exception e) {
Logger.e("tt9.LanguageCollection", "Skipping invalid language: '" + file + "'. " + e.getMessage()); Logger.e("tt9.LanguageCollection", "Skipping invalid language: '" + definition.name + "'. " + e.getMessage());
} }
} }
} }

View file

@ -8,17 +8,17 @@ import androidx.annotation.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import io.github.sspanak.tt9.BuildConfig; import io.github.sspanak.tt9.BuildConfig;
import io.github.sspanak.tt9.util.AssetFile; import io.github.sspanak.tt9.util.AssetFile;
import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.Logger;
public class LanguageDefinition extends AssetFile { public class LanguageDefinition {
private static final String LOG_TAG = LanguageDefinition.class.getSimpleName(); private static final String LOG_TAG = LanguageDefinition.class.getSimpleName();
private static final String languagesDir = "languages"; private static final String LANGUAGES_DIR = "languages";
private static final String definitionsDir = languagesDir + "/definitions"; private static final String DEFINITIONS_PATH = LANGUAGES_DIR + "/definitions.yml";
private static final String YAML_SEPARATOR = "---";
public String abcString = ""; public String abcString = "";
public String currency = ""; public String currency = "";
@ -30,186 +30,181 @@ public class LanguageDefinition extends AssetFile {
public String locale = ""; public String locale = "";
public String name = ""; public String name = "";
private boolean inLayout = false;
public LanguageDefinition(AssetManager assets, String name) {
super(assets, definitionsDir + "/" + name);
}
/** /**
* getAllFiles * Converts YAML definitions to a LanguageDefinition objects. All properties in the YAML are
* Returns a list of the paths of all language definition files in the assets folder or an empty list on error. * considered optional, so the LanguageDefinition defaults will be used when some property is omitted.
*/
public static ArrayList<String> getAllFiles(AssetManager assets) {
ArrayList<String> files = new ArrayList<>();
try {
files.addAll(Arrays.asList(assets.list(definitionsDir)));
Logger.d(LOG_TAG, "Found: " + files.size() + " languages.");
} catch (IOException | NullPointerException e) {
Logger.e(LOG_TAG, "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage());
}
return files;
}
/**
* fromFile
* Takes the path to a language definition in the assets folder and parses that file into a LanguageDefinition
* or throws an IOException on error.
*/
public static LanguageDefinition fromFile(AssetManager assetManager, String definitionFile) throws IOException {
LanguageDefinition definition = new LanguageDefinition(assetManager, definitionFile);
definition.parse(definition.load(definition));
return definition;
}
/**
* load
* Loads a language definition file from the assets folder into a String or throws an IOException on error.
*/
private ArrayList<String> load(LanguageDefinition definitionFile) throws IOException {
BufferedReader reader = definitionFile.getReader();
ArrayList<String> fileContents = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
fileContents.add(line);
}
return fileContents;
}
/**
* parse
* Converts "yaml" to a LanguageDefinition object. All properties in the YAML are considered optional,
* so the LanguageDefinition defaults will be used when some property is omitted.
* Had to write all this, because the only usable library, SnakeYAML, works fine on Android 10+, * Had to write all this, because the only usable library, SnakeYAML, works fine on Android 10+,
* but causes crashes on older devices. * but causes crashes on older devices.
*/ */
private LanguageDefinition() {}
private void parse(ArrayList<String> yaml) {
abcString = getPropertyFromYaml(yaml, "abcString", abcString);
currency = getPropertyFromYaml(yaml, "currency", currency);
dictionaryFile = getPropertyFromYaml(yaml, "dictionaryFile", dictionaryFile); /**
if (dictionaryFile != null) { * Returns a list of all language definitions contained in the asset at DEFINITIONS_PATH,
dictionaryFile = dictionaryFile.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION); * or an empty list on error.
*/
public static ArrayList<LanguageDefinition> getAll(AssetManager assets) {
String[] definitionLines = readDefinitions(assets);
if (definitionLines.length == 0) {
return new ArrayList<>();
} }
hasSpaceBetweenWords = getPropertyFromYaml(yaml, "hasSpaceBetweenWords", hasSpaceBetweenWords); ArrayList<LanguageDefinition> definitions = new ArrayList<>();
hasUpperCase = getPropertyFromYaml(yaml, "hasUpperCase", hasUpperCase);
isSyllabary = hasYamlProperty(yaml, "sounds"); LanguageDefinition definition = new LanguageDefinition();
layout = getLayoutFromYaml(yaml); for (String line : definitionLines) {
locale = getPropertyFromYaml(yaml, "locale", locale); if (YAML_SEPARATOR.equals(line)) {
name = getPropertyFromYaml(yaml, "name", name); definitions.add(definition);
definition = new LanguageDefinition();
} else if (!definition.setLayoutEntry(line)) {
definition.setProperty(line);
}
}
definitions.add(definition);
Logger.d("tt9.LanguageCollection", "Found " + definitions.size() + " languages");
return definitions;
} }
/** /**
* getPropertyFromYaml * Reads the language definitions from DEFINITIONS_PATH and returns them as an array of strings.
* Finds "property" in the "yaml" and returns its value.
* Optional properties are allowed. If the property is not found, "defaultValue" will be returned.
*/ */
@Nullable private static String[] readDefinitions(AssetManager assets) {
private String getPropertyFromYaml(ArrayList<String> yaml, String property, String defaultValue) { try {
for (String line : yaml) { BufferedReader reader = new AssetFile(assets, DEFINITIONS_PATH).getReader();
line = line.replaceAll("#.+$", "").trim(); StringBuilder contents = new StringBuilder();
String[] parts = line.split(":"); char[] buffer = new char[10000];
if (parts.length < 2) { int read;
continue;
while ((read = reader.read(buffer)) != -1) {
contents.append(buffer, 0, read);
} }
if (property.equals(parts[0].trim())) { return contents.toString().split("\n");
return parts[1].trim(); } catch (IOException e) {
} Logger.e(LOG_TAG, "Failed reading language definitions from: '" + DEFINITIONS_PATH + "'. " + e.getMessage());
return new String[0];
} }
return defaultValue;
} }
private boolean hasYamlProperty(ArrayList<String> yaml, String property) { /**
final String yamlProperty = property + ":"; * Normalizes a YAML boolean to a Java boolean.
*/
private boolean parseYamlBoolean(@Nullable String value) {
if (value == null) {
return false;
}
for (String line : yaml) { return switch (value.toLowerCase()) {
if (line.startsWith(yamlProperty)) { case "true", "on", "yes", "y" -> true;
return true; default -> false;
} };
}
/**
* Sets a property based on the key-value pair in a YAML line. If the key does not match any
* property, the line is ignored.
*/
private void setProperty(@NonNull String line) {
int colonIndex = line.indexOf(':');
if (colonIndex == -1) {
return;
}
String key = line.substring(0, colonIndex).trim();
String value = (colonIndex + 1 < line.length()) ? line.substring(colonIndex + 1).trim() : "";
switch (key) {
case "abcString":
abcString = value;
break;
case "currency":
currency = value;
break;
case "dictionaryFile":
dictionaryFile = value.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION);
break;
case "hasSpaceBetweenWords":
hasSpaceBetweenWords = parseYamlBoolean(value);
break;
case "hasUpperCase":
hasUpperCase = parseYamlBoolean(value);
break;
case "sounds":
isSyllabary = true;
break;
case "locale":
locale = value;
break;
case "name":
name = value;
break;
}
}
/**
* Builds the key layout line by line. Returns true when a layout entry is successfully set.
*/
private boolean setLayoutEntry(@NonNull String line) {
if (!inLayout) {
return inLayout = "layout:".equals(line);
}
ArrayList<String> layoutEntry = getLayoutEntryFromYamlLine(line);
if (layoutEntry == null) {
inLayout = false;
} else {
layout.add(layoutEntry);
return true;
} }
return false; return false;
} }
/**
* The boolean variant of getPropertyFromYaml. It returns true if the property is found and is:
* "true", "on", "yes" or "y".
*/
private boolean getPropertyFromYaml(ArrayList<String> yaml, String property, boolean defaultValue) {
String value = getPropertyFromYaml(yaml, property, null);
if (value == null) {
return defaultValue;
}
value = value.toLowerCase();
return value.equals("true") || value.equals("on") || value.equals("yes") || value.equals("y");
}
/**
* getLayoutFromYaml
* Finds and extracts the keypad layout. Less than 10 keys are accepted allowed leaving the ones up to 9-key empty.
*/
@NonNull
private ArrayList<ArrayList<String>> getLayoutFromYaml(ArrayList<String> yaml) {
ArrayList<ArrayList<String>> layout = new ArrayList<>();
boolean inLayout = false;
for (int i = 0; i < yaml.size(); i++) {
if (yaml.get(i).contains("layout")) {
inLayout = true;
continue;
}
if (inLayout) {
ArrayList<String> lineChars = getLayoutEntryFromYamlLine(yaml.get(i));
if (lineChars != null) {
layout.add(lineChars);
} else {
break;
}
}
}
return layout;
}
/** /**
* getLayoutEntryFromYamlLine * getLayoutEntryFromYamlLine
* Validates a YAML line as an array and returns the character list to be assigned to a given key (a layout entry). * Validates a YAML line as an array and returns the character list to be assigned to a given key (a layout entry).
* If the YAML line is invalid, NULL will be returned. * If the YAML line is invalid, NULL will be returned.
*/ */
@Nullable @Nullable
private ArrayList<String> getLayoutEntryFromYamlLine(String yamlLine) { private ArrayList<String> getLayoutEntryFromYamlLine(@NonNull String yamlLine) {
if (!yamlLine.contains("[") || !yamlLine.contains("]")) { int start = yamlLine.indexOf('[');
int end = yamlLine.indexOf(']');
if (start == -1 || end == -1 || start >= end) {
return null; return null;
} }
String line = yamlLine String entryTxt = yamlLine.substring(start + 1, end).replace(" ", "");
.replaceAll("#.+$", "")
.replace('-', ' ')
.replace('[', ' ')
.replace(']', ' ')
.replace(" ", "");
return new ArrayList<>(Arrays.asList(line.split(","))); ArrayList<String> entry = new ArrayList<>();
int last = 0, len = entryTxt.length();
for (int i = 0; i < len; i++) {
if (entryTxt.charAt(i) == ',') {
entry.add(entryTxt.substring(last, i));
last = i + 1;
}
}
if (last < len) {
entry.add(entryTxt.substring(last));
}
return entry;
} }
public String getDictionaryFile() { public String getDictionaryFile() {
return languagesDir + "/dictionaries/" + dictionaryFile; return LANGUAGES_DIR + "/dictionaries/" + dictionaryFile;
} }
} }