diff --git a/build.gradle b/build.gradle index 2d516bba..fd20748d 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.room:room-runtime:2.5.1' annotationProcessor 'androidx.room:room-compiler:2.5.1' - implementation 'org.yaml:snakeyaml:2.0' } repositories { diff --git a/proguard-project.txt b/proguard-project.txt index 3d09f0ca..5bf49af3 100644 --- a/proguard-project.txt +++ b/proguard-project.txt @@ -24,11 +24,3 @@ -keepclassmembers class io.github.sspanak.tt9.languages.LanguageDefinition { public *; } - -# SnakeYAML shall not complain. As resolved by Mozilla: -# https://github.com/mozilla-mobile/focus-android/blob/main/app/proguard-rules.pro --dontwarn java.beans.BeanInfo --dontwarn java.beans.FeatureDescriptor --dontwarn java.beans.IntrospectionException --dontwarn java.beans.Introspector --dontwarn java.beans.PropertyDescriptor diff --git a/src/io/github/sspanak/tt9/languages/Language.java b/src/io/github/sspanak/tt9/languages/Language.java index f524e859..28ecf03b 100644 --- a/src/io/github/sspanak/tt9/languages/Language.java +++ b/src/io/github/sspanak/tt9/languages/Language.java @@ -61,7 +61,7 @@ public class Language { lang.locale = definitionLocale; lang.name = definition.name.isEmpty() ? lang.name : definition.name; - for (int key = 0; key <= 9 || key < definition.layout.size(); key++) { + for (int key = 0; key <= 9 && key < definition.layout.size(); key++) { lang.layout.add(keyCharsFromDefinition(key, definition.layout.get(key))); } diff --git a/src/io/github/sspanak/tt9/languages/LanguageDefinition.java b/src/io/github/sspanak/tt9/languages/LanguageDefinition.java index 7606be3a..5b896b79 100644 --- a/src/io/github/sspanak/tt9/languages/LanguageDefinition.java +++ b/src/io/github/sspanak/tt9/languages/LanguageDefinition.java @@ -2,13 +2,15 @@ package io.github.sspanak.tt9.languages; import android.content.res.AssetManager; -import org.yaml.snakeyaml.Yaml; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import io.github.sspanak.tt9.Logger; @@ -23,12 +25,19 @@ public class LanguageDefinition { public String locale = ""; public String 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 getAllFiles(AssetManager assets) { ArrayList files = new ArrayList<>(); try { for (String file : assets.list(definitionsDir)) { files.add(definitionsDir + "/" + file); } + + Logger.d("LanguageDefinition", "Found: " + files.size() + " languages."); } catch (IOException e) { Logger.e("tt9.LanguageDefinition", "Failed reading language definitions from: '" + definitionsDir + "'. " + e.getMessage()); } @@ -36,11 +45,144 @@ public class LanguageDefinition { return files; } - public static LanguageDefinition fromFile(AssetManager assets, String definitionFile) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(definitionFile), StandardCharsets.UTF_8)); - return new Yaml().loadAs(reader, LanguageDefinition.class); + + /** + * 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 { + return parse(load(assetManager, definitionFile)); } + + /** + * load + * Loads a language definition file from the assets folder into a String or throws an IOException on error. + */ + private static ArrayList load(AssetManager assetManager, String definitionFile) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open(definitionFile), StandardCharsets.UTF_8)); + ArrayList 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+, + * but causes crashes on older devices. + */ + @NonNull + private static LanguageDefinition parse(ArrayList yaml) { + LanguageDefinition definition = new LanguageDefinition(); + String value; + + value = getPropertyFromYaml(yaml, "absString"); + definition.abcString = value != null ? value : definition.abcString; + + value = getPropertyFromYaml(yaml, "dictionaryFile"); + definition.dictionaryFile = value != null ? value : definition.dictionaryFile; + + value = getPropertyFromYaml(yaml, "locale"); + definition.locale = value != null ? value : definition.locale; + + value = getPropertyFromYaml(yaml, "name"); + definition.name = value != null ? value : definition.name; + + definition.layout = getLayoutFromYaml(yaml); + + value = getPropertyFromYaml(yaml, "hasUpperCase"); + if (value != null) { + value = value.toLowerCase(); + definition.hasUpperCase = value.equals("true") || value.equals("on") || value.equals("yes") || value.equals("y"); + } + + return definition; + } + + + /** + * getPropertyFromYaml + * Finds "property" in the "yaml" and returns its value. + * Optional properties are allowed. NULL will be returned when they are missing. + */ + @Nullable + private static String getPropertyFromYaml(ArrayList yaml, String property) { + for (String line : yaml) { + line = line.replaceAll("#.+$", "").trim(); + String[] parts = line.split(":"); + if (parts.length < 2) { + continue; + } + + if (property.equals(parts[0].trim())) { + return parts[1].trim(); + } + } + + return null; + } + + + /** + * getLayoutFromYaml + * Finds and extracts the keypad layout. Less than 10 keys are accepted allowed leaving the ones up to 9-key empty. + */ + @NonNull + private static ArrayList> getLayoutFromYaml(ArrayList yaml) { + ArrayList> 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 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 static ArrayList getLayoutEntryFromYamlLine(String yamlLine) { + if (!yamlLine.contains("[") || !yamlLine.contains("]")) { + return null; + } + + String line = yamlLine + .replaceAll("#.+$", "") + .replace('-', ' ') + .replace('[', ' ') + .replace(']', ' ') + .replace(" ", ""); + + return new ArrayList<>(Arrays.asList(line.split(","))); + } + + public String getDictionaryFile() { return languagesDir + "/dictionaries/" + dictionaryFile; }