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:
parent
ae619e1f0f
commit
c844db1fa1
7 changed files with 180 additions and 157 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -29,7 +29,9 @@ jobs:
|
|||
- name: Validate Dictionaries
|
||||
run: ./gradlew validateLanguages
|
||||
- name: Build Languages
|
||||
run: ./gradlew copyDefinitions convertHelp buildDictionaryDownloads copyDownloadsToAssets
|
||||
run: ./gradlew buildDefinition buildDictionaryDownloads
|
||||
- name: Copy Downloads
|
||||
run: ./gradlew copyDownloadsToAssets
|
||||
- name: Lint
|
||||
run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks
|
||||
- name: Build all APK variants
|
||||
|
|
|
|||
22
app/build-definitions.gradle
Normal file
22
app/build-definitions.gradle
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,13 @@ plugins {
|
|||
}
|
||||
|
||||
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: 'help-tools.gradle'
|
||||
apply from: 'version-tools.gradle'
|
||||
|
||||
|
||||
tasks.register('copyDefinitions', Copy) {
|
||||
from LANGUAGES_INPUT_DIR
|
||||
include '**/*.yml'
|
||||
into LANGUAGES_OUTPUT_DIR
|
||||
}
|
||||
|
||||
|
||||
tasks.register('validateLanguages') {
|
||||
inputs.dir LANGUAGES_INPUT_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') {
|
||||
inputs.dir DICTIONARIES_INPUT_DIR
|
||||
inputs.dir LANGUAGES_INPUT_DIR
|
||||
outputs.dir DICTIONARIES_DOWNLOAD_DIR
|
||||
outputs.dir DICTIONARY_META_OUTPUT_DIR
|
||||
|
||||
|
|
@ -139,7 +143,7 @@ android {
|
|||
].each { taskName ->
|
||||
try {
|
||||
tasks.named(taskName)?.configure {
|
||||
dependsOn(copyDefinitions, convertHelp, validateLanguages, buildDictionaryDownloads)
|
||||
dependsOn(buildDefinition, convertHelp, validateLanguages, buildDictionaryDownloads)
|
||||
}
|
||||
|
||||
if (taskName.toLowerCase().contains("full")) {
|
||||
|
|
|
|||
|
|
@ -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.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.DICTIONARIES_OUTPUT_DIR = "${FULL_VERSION_ASSETS_DIR}/${LANGUAGES_DIR_NAME}/${DICTIONARIES_DIR_NAME}"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ public class LanguageCollection {
|
|||
private final HashMap<Integer, NaturalLanguage> languages = new HashMap<>();
|
||||
|
||||
private LanguageCollection(Context context) {
|
||||
for (String file : LanguageDefinition.getAllFiles(context.getAssets())) {
|
||||
for (LanguageDefinition definition : LanguageDefinition.getAll(context.getAssets())) {
|
||||
try {
|
||||
NaturalLanguage lang = NaturalLanguage.fromDefinition(LanguageDefinition.fromFile(context.getAssets(), file));
|
||||
NaturalLanguage lang = NaturalLanguage.fromDefinition(definition);
|
||||
languages.put(lang.getId(), lang);
|
||||
} catch (Exception e) {
|
||||
Logger.e("tt9.LanguageCollection", "Skipping invalid language: '" + file + "'. " + e.getMessage());
|
||||
Logger.e("tt9.LanguageCollection", "Skipping invalid language: '" + definition.name + "'. " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ import androidx.annotation.Nullable;
|
|||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import io.github.sspanak.tt9.BuildConfig;
|
||||
import io.github.sspanak.tt9.util.AssetFile;
|
||||
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 languagesDir = "languages";
|
||||
private static final String definitionsDir = languagesDir + "/definitions";
|
||||
private static final String LANGUAGES_DIR = "languages";
|
||||
private static final String DEFINITIONS_PATH = LANGUAGES_DIR + "/definitions.yml";
|
||||
private static final String YAML_SEPARATOR = "---";
|
||||
|
||||
public String abcString = "";
|
||||
public String currency = "";
|
||||
|
|
@ -30,186 +30,181 @@ public class LanguageDefinition extends AssetFile {
|
|||
public String locale = "";
|
||||
public String name = "";
|
||||
|
||||
private boolean inLayout = false;
|
||||
|
||||
public LanguageDefinition(AssetManager assets, String name) {
|
||||
super(assets, definitionsDir + "/" + name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getAllFiles
|
||||
* Returns a list of the paths of all language definition files in the assets folder or an empty list on error.
|
||||
*/
|
||||
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.
|
||||
* Converts YAML definitions to a LanguageDefinition objects. 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+,
|
||||
* 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) {
|
||||
dictionaryFile = dictionaryFile.replaceFirst("\\.\\w+$", "." + BuildConfig.DICTIONARY_EXTENSION);
|
||||
/**
|
||||
* Returns a list of all language definitions contained in the asset at DEFINITIONS_PATH,
|
||||
* 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);
|
||||
hasUpperCase = getPropertyFromYaml(yaml, "hasUpperCase", hasUpperCase);
|
||||
isSyllabary = hasYamlProperty(yaml, "sounds");
|
||||
layout = getLayoutFromYaml(yaml);
|
||||
locale = getPropertyFromYaml(yaml, "locale", locale);
|
||||
name = getPropertyFromYaml(yaml, "name", name);
|
||||
ArrayList<LanguageDefinition> definitions = new ArrayList<>();
|
||||
|
||||
LanguageDefinition definition = new LanguageDefinition();
|
||||
for (String line : definitionLines) {
|
||||
if (YAML_SEPARATOR.equals(line)) {
|
||||
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
|
||||
* Finds "property" in the "yaml" and returns its value.
|
||||
* Optional properties are allowed. If the property is not found, "defaultValue" will be returned.
|
||||
* Reads the language definitions from DEFINITIONS_PATH and returns them as an array of strings.
|
||||
*/
|
||||
@Nullable
|
||||
private String getPropertyFromYaml(ArrayList<String> yaml, String property, String defaultValue) {
|
||||
for (String line : yaml) {
|
||||
line = line.replaceAll("#.+$", "").trim();
|
||||
String[] parts = line.split(":");
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
private static String[] readDefinitions(AssetManager assets) {
|
||||
try {
|
||||
BufferedReader reader = new AssetFile(assets, DEFINITIONS_PATH).getReader();
|
||||
StringBuilder contents = new StringBuilder();
|
||||
char[] buffer = new char[10000];
|
||||
int read;
|
||||
|
||||
while ((read = reader.read(buffer)) != -1) {
|
||||
contents.append(buffer, 0, read);
|
||||
}
|
||||
|
||||
if (property.equals(parts[0].trim())) {
|
||||
return parts[1].trim();
|
||||
}
|
||||
return contents.toString().split("\n");
|
||||
} 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) {
|
||||
if (line.startsWith(yamlProperty)) {
|
||||
return true;
|
||||
}
|
||||
return switch (value.toLowerCase()) {
|
||||
case "true", "on", "yes", "y" -> 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
private ArrayList<String> getLayoutEntryFromYamlLine(String yamlLine) {
|
||||
if (!yamlLine.contains("[") || !yamlLine.contains("]")) {
|
||||
private ArrayList<String> getLayoutEntryFromYamlLine(@NonNull String yamlLine) {
|
||||
int start = yamlLine.indexOf('[');
|
||||
int end = yamlLine.indexOf(']');
|
||||
if (start == -1 || end == -1 || start >= end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String line = yamlLine
|
||||
.replaceAll("#.+$", "")
|
||||
.replace('-', ' ')
|
||||
.replace('[', ' ')
|
||||
.replace(']', ' ')
|
||||
.replace(" ", "");
|
||||
String entryTxt = yamlLine.substring(start + 1, end).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() {
|
||||
return languagesDir + "/dictionaries/" + dictionaryFile;
|
||||
return LANGUAGES_DIR + "/dictionaries/" + dictionaryFile;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue