From 6670bccc503c9a3d8b79cd61e0380e789dffc8d0 Mon Sep 17 00:00:00 2001 From: sspanak Date: Wed, 12 Jun 2024 12:48:55 +0300 Subject: [PATCH] 'full' and 'lite' flavors --- .github/workflows/build.yml | 4 +- .gitignore | 2 + README.md | 2 +- app/build.gradle | 47 ++++--- app/constants.gradle | 8 +- app/dictionary-tools.gradle | 16 +-- app/src/lite/AndroidManifest.xml | 3 + .../sspanak/tt9/db/DictionaryLoader.java | 16 +-- .../sspanak/tt9/db/entities/WordFile.java | 126 +++++++++++++++--- .../screens/languages/ItemLoadDictionary.java | 2 +- .../preferences/settings/SettingsStore.java | 2 + .../notifications/DictionaryLoadingBar.java | 4 + app/src/main/res/values/strings.xml | 2 + app/version-tools.gradle | 4 + .../android/bg-BG/full_description.txt | 4 +- .../android/en-US/full_description.txt | 4 +- 16 files changed, 182 insertions(+), 64 deletions(-) create mode 100644 app/src/lite/AndroidManifest.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fcb609e..5c1ffdce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,8 +27,8 @@ jobs: - name: Validate Dictionaries run: ./gradlew validateLanguages - name: Build Languages - run: ./gradlew copyLanguages writeDictionaryProperties + run: ./gradlew copyDefinitions copyDictionaries writeDictionaryProperties - name: Lint run: ./gradlew lint # this actually runs mergeResources, so it must come after the dictionary tasks - - name: Build Release APK + - name: Build all APK variants run: ./gradlew build diff --git a/.gitignore b/.gitignore index 6c2841e6..d22e7cde 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ gen/ .gradle/ assets/ build/ +full/ +lite/ release/ # Local configuration file (sdk path, etc) diff --git a/README.md b/README.md index 9b6025c7..716a7585 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Thanks to your donations, a brand new testing device is available, a Sonim XP380 ## πŸ’ͺ Privacy Policy and Philosophy - No ads, no premium or paid features. It's all free. - No spying, no tracking, no telemetry or reports. No nothing! -- No network connectivity, except when voice input is active. +- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission. - It only does its job. - Open-source, so you can verify all the above yourself. - Created with help from the entire community. diff --git a/app/build.gradle b/app/build.gradle index 6a4adb10..db45114f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,22 +17,25 @@ tasks.register('validateLanguages') { } } -tasks.register('copyLanguages', Copy) { +tasks.register('copyDefinitions', Copy) { from LANGUAGES_INPUT_DIR - include '**/*.csv' - include '**/*.txt' include '**/*.yml' into LANGUAGES_OUTPUT_DIR } +tasks.register('copyDictionaries', Copy) { + from DICTIONARIES_INPUT_DIR + include '**/*.csv' + include '**/*.txt' + into DICTIONARIES_OUTPUT_DIR +} + tasks.register('writeDictionaryProperties') { inputs.dir fileTree(dir: DICTIONARIES_INPUT_DIR) - outputs.dir DICTIONARIES_OUTPUT_DIR + outputs.dir DICTIONARY_META_OUTPUT_DIR doLast { - [getDictionarySizes, getDictionaryHashes].parallelStream().forEach { action -> - action(DICTIONARIES_INPUT_DIR, DICTIONARIES_OUTPUT_DIR) - } + getDictionaryProperties(DICTIONARIES_INPUT_DIR, DICTIONARY_META_OUTPUT_DIR) } } @@ -44,6 +47,7 @@ tasks.register('updateManifest') { clean { delete LANGUAGES_OUTPUT_DIR + delete DICTIONARIES_OUTPUT_DIR } // using the exported Closures directly causes weird values, hence the extra wrappers here @@ -87,24 +91,35 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - applicationVariants.configureEach { variant -> - tasks.named("generate${variant.name.capitalize()}Assets")?.configure { - dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties) - } + flavorDimensions = ['app'] + productFlavors { + full { dimension 'app' } + lite { dimension 'app' } + } - ["lintAnalyzeDebug", "generateDebugLintReportModel", "lintVitalAnalyzeRelease", "generateReleaseLintVitalReportModel"].each { taskName -> + applicationVariants.configureEach { variant -> + [ + "merge${variant.name.capitalize()}Assets", + "lintAnalyze${variant.name.capitalize()}", + "generate${variant.name.capitalize()}LintReportModel", + "lintVitalAnalyze${variant.name.capitalize()}", + "generate${variant.name.capitalize()}LintVitalReportModel" + ].each { taskName -> try { tasks.named(taskName)?.configure { - dependsOn(validateLanguages, copyLanguages, writeDictionaryProperties) + dependsOn(validateLanguages, copyDefinitions, copyDictionaries, writeDictionaryProperties) } } catch (UnknownTaskException ignored) {} } - assembleDebug.finalizedBy(updateManifest) - assembleRelease.finalizedBy(updateManifest) + assembleLiteDebug.finalizedBy(updateManifest) + assembleFullDebug.finalizedBy(updateManifest) + assembleLiteRelease.finalizedBy(updateManifest) + assembleFullRelease.finalizedBy(updateManifest) variant.outputs.configureEach { - outputFileName = "${APP_NAME}-v${getVerName()}.apk" + def suffix = variant.flavorName == 'full' ? '-full' : '' + outputFileName = "${APP_NAME}-v${getVerName()}${suffix}.apk" } } } diff --git a/app/constants.gradle b/app/constants.gradle index 31afbc89..b715549d 100644 --- a/app/constants.gradle +++ b/app/constants.gradle @@ -7,15 +7,17 @@ ext.DICTIONARIES_DIR_NAME = 'dictionaries' ext.DICTIONARY_SIZES_DIR_NAME = 'dictionary-sizes' def ROOT_DIR = "${project.rootDir}/app" -def ASSETS_DIR = "${ROOT_DIR}/src/main/assets" +def MAIN_ASSETS_DIR = "${ROOT_DIR}/src/main/assets" +def FULL_VERSION_ASSETS_DIR = "${ROOT_DIR}/src/full/assets" ext.LANGUAGES_INPUT_DIR = "${ROOT_DIR}/${LANGUAGES_DIR_NAME}" ext.DEFINITIONS_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DEFINITIONS_DIR_NAME}" ext.DICTIONARIES_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DICTIONARIES_DIR_NAME}" -ext.LANGUAGES_OUTPUT_DIR = "${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.DICTIONARIES_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.LANGUAGE_VALIDATION_DIR = layout.buildDirectory.dir("langValidation") diff --git a/app/dictionary-tools.gradle b/app/dictionary-tools.gradle index d3a01f5b..57844f9b 100644 --- a/app/dictionary-tools.gradle +++ b/app/dictionary-tools.gradle @@ -1,13 +1,9 @@ -ext.getDictionarySizes = { dictionariesDir, sizesDir -> - fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary -> - def dictionarySize = dictionary.exists() ? dictionary.text.split("\n").length : 0 - new File(sizesDir, "${dictionary.getName()}.size").text = dictionarySize - } -} - -ext.getDictionaryHashes = { dictionariesDir, timestampsDir -> +ext.getDictionaryProperties = { dictionariesDir, sizesDir -> fileTree(dir: dictionariesDir).getFiles().parallelStream().forEach {dictionary -> def hash = dictionary.exists() ? dictionary.text.digest("SHA-1") : "" - new File(timestampsDir, "${dictionary.getName()}.hash").text = hash + def revision = dictionary.exists() ? exec("git log --pretty=tformat:%H -n 1 ${dictionary}") : "" + def words = dictionary.exists() ? dictionary.text.split("\n").length : 0 + + new File(sizesDir, "${dictionary.getName()}.props.yml").text = "hash: ${hash}\nrevision: ${revision}\nwords: ${words}" } -} +} \ No newline at end of file diff --git a/app/src/lite/AndroidManifest.xml b/app/src/lite/AndroidManifest.xml new file mode 100644 index 00000000..e0e81ba4 --- /dev/null +++ b/app/src/lite/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java index 20fcb467..8e4942ce 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DictionaryLoader.java @@ -68,7 +68,7 @@ public class DictionaryLoader { } - public boolean load(ArrayList languages) { + public boolean load(Context context, ArrayList languages) { if (isRunning()) { return false; } @@ -91,7 +91,7 @@ public class DictionaryLoader { if (isInterrupted()) { break; } - importAll(lang); + importAll(context, lang); currentFile++; } } @@ -105,7 +105,7 @@ public class DictionaryLoader { public static void load(Context context, Language language) { DictionaryLoadingBar progressBar = DictionaryLoadingBar.getInstance(context); getInstance(context).setOnStatusChange(status -> progressBar.show(context, status)); - self.load(new ArrayList() {{ add(language); }}); + self.load(context, new ArrayList() {{ add(language); }}); } @@ -129,7 +129,7 @@ public class DictionaryLoader { load(context, language); } // or if the database is outdated, compared to the dictionary file, ask for confirmation and load - else if (!hash.equals(new WordFile(language.getDictionaryFile(), self.assets).getHash())) { + else if (!hash.equals(new WordFile(context, language.getDictionaryFile(), self.assets).getHash())) { new DictionaryUpdateNotification(context, language).show(); } }, @@ -151,7 +151,7 @@ public class DictionaryLoader { } - private void importAll(Language language) { + private void importAll(Context context, Language language) { if (language == null) { Logger.e(LOG_TAG, "Failed loading a dictionary for NULL language."); sendError(InvalidLanguageException.class.getSimpleName(), -1); @@ -178,7 +178,7 @@ public class DictionaryLoader { sendProgressMessage(language, ++progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME); logLoadingStep("Letters imported", language, Timer.restart()); - importWordFile(language, lettersCount, progress, 88); + importWordFile(context, language, lettersCount, progress, 88); progress = 88; sendProgressMessage(language, progress, SettingsStore.DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME); logLoadingStep("Dictionary file imported", language, Timer.restart()); @@ -252,8 +252,8 @@ public class DictionaryLoader { } - private void importWordFile(Language language, int positionShift, float minProgress, float maxProgress) throws Exception { - WordFile wordFile = new WordFile(language.getDictionaryFile(), assets); + private void importWordFile(Context context, Language language, int positionShift, float minProgress, float maxProgress) throws Exception { + WordFile wordFile = new WordFile(context, language.getDictionaryFile(), assets); WordBatch batch = new WordBatch(language, wordFile.getTotalLines()); int currentLine = 1; float progressRatio = (maxProgress - minProgress) / wordFile.getTotalLines(); diff --git a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java index b2ae39cc..db5a0308 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/entities/WordFile.java @@ -1,27 +1,39 @@ package io.github.sspanak.tt9.db.entities; +import android.content.Context; import android.content.res.AssetManager; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; +import io.github.sspanak.tt9.BuildConfig; +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.Logger; public class WordFile { private static final String LOG_TAG = WordFile.class.getSimpleName(); private final AssetManager assets; + private final Context context; private final String name; private String hash = null; + private String downloadUrl = null; private int totalLines = -1; - public WordFile(String name, AssetManager assets) { + + public WordFile(Context context, String name, AssetManager assets) { this.assets = assets; + this.context = context; this.name = name; } + public static String[] splitLine(String line) { String[] parts = { line, "" }; @@ -38,6 +50,7 @@ public class WordFile { return parts; } + public static short getFrequencyFromLineParts(String[] frequencyParts) { try { return Short.parseShort(frequencyParts[1]); @@ -46,40 +59,115 @@ public class WordFile { } } - public BufferedReader getReader() throws IOException { - return new BufferedReader(new InputStreamReader(assets.open(name), StandardCharsets.UTF_8)); + + public boolean exists() { + try { + assets.open(name).close(); + return true; + } catch (IOException e) { + return false; + } } - public int getTotalLines() { - if (totalLines < 0) { - String rawTotalLines = getProperty("size"); - try { - totalLines = Integer.parseInt(rawTotalLines); - } catch (Exception e) { - Logger.w(LOG_TAG, "Invalid 'size' property of: " + name + ". Expecting an integer, got: '" + rawTotalLines + "'."); - totalLines = 0; - } + + public InputStream getRemoteStream() throws IOException { + URLConnection connection = new URL(getDownloadUrl()).openConnection(); + connection.setConnectTimeout(SettingsStore.DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT); + connection.setReadTimeout(SettingsStore.DICTIONARY_DOWNLOAD_READ_TIMEOUT); + return connection.getInputStream(); + } + + + public BufferedReader getReader() throws IOException { + InputStream stream = exists() ? assets.open(name) : getRemoteStream(); + return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } + + + private String getDownloadUrl() { + if (downloadUrl == null) { + loadProperties(); } - return totalLines; + return downloadUrl; } + + private void setDownloadUrl(String rawProperty, String rawValue) { + if (!rawProperty.equals("revision")) { + return; + } + + String revision = rawValue == null || rawValue.isEmpty() ? "" : rawValue; + downloadUrl = revision.isEmpty() ? null : context.getString(R.string.dictionary_url, revision, name); + + if (revision.isEmpty()) { + Logger.w(LOG_TAG, "Invalid 'revision' property of: " + name + ". Expecting a string, got: '" + rawValue + "'."); + } + } + + public String getHash() { if (hash == null) { - hash = getProperty("hash"); + loadProperties(); } return hash; } - private String getProperty(String propertyName) { - String propertyFilename = name + "." + propertyName; + + private void setHash(String rawProperty, String rawValue) { + if (!rawProperty.equals("hash")) { + return; + } + + hash = rawValue == null || rawValue.isEmpty() ? "" : rawValue; + + if (hash.isEmpty()) { + Logger.w(LOG_TAG, "Invalid 'hash' property of: " + name + ". Expecting a string, got: '" + rawValue + "'."); + } + } + + + public int getTotalLines() { + if (totalLines < 0) { + loadProperties(); + } + + return totalLines; + } + + + private void setTotalLines(String rawProperty, String rawValue) { + if (!rawProperty.equals("words")) { + return; + } + + try { + totalLines = Integer.parseInt(rawValue); + } catch (Exception e) { + Logger.w(LOG_TAG, "Invalid 'words' property of: " + name + ". Expecting an integer, got: '" + rawValue + "'."); + totalLines = 0; + } + } + + + private void loadProperties() { + String propertyFilename = name + ".props.yml"; try (BufferedReader reader = new BufferedReader(new InputStreamReader(assets.open(propertyFilename)))) { - return reader.readLine(); + for (String line; (line = reader.readLine()) != null; ) { + String[] parts = line.split("\\s*:\\s*"); + if (parts.length < 2) { + continue; + } + + setDownloadUrl(parts[0], parts[1]); + setHash(parts[0], parts[1]); + setTotalLines(parts[0], parts[1]); + } } catch (Exception e) { - Logger.w(LOG_TAG, "Could not read the '" + propertyName + "' property of: " + name + " from: " + propertyFilename + ". " + e.getMessage()); - return ""; + Logger.w(LOG_TAG, "Could not read the property file: " + propertyFilename + ". " + e.getMessage()); } } } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java index 275db768..ef59d892 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languages/ItemLoadDictionary.java @@ -70,7 +70,7 @@ class ItemLoadDictionary extends ItemClickable { ArrayList languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds()); setLoadingStatus(); - if (!loader.load(languages)) { + if (!loader.load(activity, languages)) { loader.stop(); setReadyStatus(); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java index 10393beb..a4aeadc8 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java @@ -9,6 +9,8 @@ public class SettingsStore extends SettingsUI { /************* internal settings *************/ public final static int DELETE_WORDS_SEARCH_DELAY = 500; // ms public final static int DICTIONARY_AUTO_LOAD_COOLDOWN_TIME = 1200000; // 20 minutes in ms + public final static int DICTIONARY_DOWNLOAD_CONNECTION_TIMEOUT = 10000; // ms + public final static int DICTIONARY_DOWNLOAD_READ_TIMEOUT = 10000; // ms public final static int DICTIONARY_IMPORT_BATCH_SIZE = 5000; // words public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms public final static byte SLOW_QUERY_TIME = 50; // ms diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java index 75c69921..6d901da3 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/notifications/DictionaryLoadingBar.java @@ -5,6 +5,8 @@ import android.os.Bundle; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.SocketException; +import java.net.UnknownHostException; import java.util.Locale; import io.github.sspanak.tt9.R; @@ -138,6 +140,8 @@ public class DictionaryLoadingBar extends DictionaryProgressNotification { message = resources.getString(R.string.add_word_invalid_language); } else if (errorType.equals(DictionaryImportException.class.getSimpleName()) || errorType.equals(InvalidLanguageCharactersException.class.getSimpleName())) { message = resources.getString(R.string.dictionary_load_bad_char, word, line, lang.getName()); + } else if (errorType.equals(UnknownHostException.class.getSimpleName()) || errorType.equals(SocketException.class.getSimpleName())) { + message = resources.getString(R.string.dictionary_load_no_internet, lang.getName()); } else if (errorType.equals(IOException.class.getSimpleName()) || errorType.equals(FileNotFoundException.class.getSimpleName())) { message = resources.getString(R.string.dictionary_not_found, lang.getName()); } else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 262d7d75..d3f82be9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s https://github.com/sspanak/tt9/blob/master/docs/user-manual.md Traditional T9 TT9 @@ -80,6 +81,7 @@ Cancel Loading Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\". Failed loading the dictionary for language \"%1$s\" (%2$s). + Failed downloading the dictionary for language \"%1$s\". Check your Internet connection. Dictionary load cancelled. Dictionary load completed. Loading dictionary (%1$s)… diff --git a/app/version-tools.gradle b/app/version-tools.gradle index 374b2f63..2010f643 100644 --- a/app/version-tools.gradle +++ b/app/version-tools.gradle @@ -44,6 +44,10 @@ def generateVersionName() { return "$versionTagsCount.$commitsSinceLastTag$betaString" } +ext.exec = { command -> + return execThing(command) +} + ext.getVersionName = { -> return generateVersionName() } diff --git a/fastlane/metadata/android/bg-BG/full_description.txt b/fastlane/metadata/android/bg-BG/full_description.txt index bbee1660..37854f86 100644 --- a/fastlane/metadata/android/bg-BG/full_description.txt +++ b/fastlane/metadata/android/bg-BG/full_description.txt @@ -1,11 +1,11 @@ -Traditional T9 Π΅ 12-клавишна ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° Π·Π° устройства с ΠΊΠΎΠΏΡ‡Π΅Ρ‚Π°. ΠŸΠΎΠ΄Π΄ΡŠΡ€ΠΆΠ° подскаващ тСкста Π½Π° ΠΏΠΎΠ²Π΅Ρ‡Π΅ ΠΎΡ‚ 25 Π΅Π·ΠΈΠΊΠ° ΠΈ Π±ΡŠΡ€Π·ΠΈ клавиши, Π° Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»Π½Π°Ρ‚Π° ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° ΠΏΡ€Π΅ΡΡŠΠ·Π΄Π°Π²Π° Π΄ΠΎΠ±Ρ€ΠΎΡ‚ΠΎ старо усСщанС Π·Π° Нокия Π½Π° ΡΡŠΠ²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΈΡ‚Π΅ Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½ΠΈ със сСнзорСн Π΅ΠΊΡ€Π°Π½. И Π½Π°ΠΉ-Ρ…ΡƒΠ±Π°Π²ΠΎΡ‚ΠΎ Π΅, Ρ‡Π΅ Π½Π΅ Π²ΠΈ ΡˆΠΏΠΈΠΎΠ½ΠΈΡ€Π°. +Traditional T9 Π΅ 12-клавишна ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° Π·Π° устройства с ΠΊΠΎΠΏΡ‡Π΅Ρ‚Π°. ΠŸΠΎΠ΄Π΄ΡŠΡ€ΠΆΠ° подскаващ тСкст Π½Π° ΠΏΠΎΠ²Π΅Ρ‡Π΅ ΠΎΡ‚ 25 Π΅Π·ΠΈΠΊΠ° ΠΈ Π±ΡŠΡ€Π·ΠΈ клавиши, Π° Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»Π½Π°Ρ‚Π° ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° ΠΏΡ€Π΅ΡΡŠΠ·Π΄Π°Π²Π° Π΄ΠΎΠ±Ρ€ΠΎΡ‚ΠΎ старо усСщанС Π·Π° Нокия Π½Π° ΡΡŠΠ²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΈΡ‚Π΅ Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½ΠΈ със сСнзорСн Π΅ΠΊΡ€Π°Π½. И Π½Π°ΠΉ-Ρ…ΡƒΠ±Π°Π²ΠΎΡ‚ΠΎ Π΅, Ρ‡Π΅ Π½Π΅ Π²ΠΈ ΡˆΠΏΠΈΠΎΠ½ΠΈΡ€Π°. ΠŸΠΎΠ΄Π΄ΡŠΡ€ΠΆΠ°Π½ΠΈ Π΅Π·ΠΈΡ†ΠΈ: арабски, Π±ΡŠΠ»Π³Π°Ρ€ΡΠΊΠΈ, Ρ…ΡŠΡ€Π²Π°Ρ‚ΡΠΊΠΈ, Ρ‡Π΅ΡˆΠΊΠΈ, датски, холандски, английски, финландски, фрСнски, нСмски, Π³Ρ€ΡŠΡ†ΠΊΠΈ, ΠΈΠ²Ρ€ΠΈΡ‚, унгарски, индонСзийски, италиански, кисуахили, Π½ΠΎΡ€Π²Π΅ΠΆΠΊΠΈ, полски, португалски (СвропСйски ΠΈ бразилски), Ρ€ΡƒΠΌΡŠΠ½ΡΠΊΠΈ, руски, испански, швСдски, турски, украински, идиш. Ѐилософия ΠΈ Π·Π°Ρ‰ΠΈΡ‚Π° Π½Π΅ Π»ΠΈΡ‡Π½ΠΈΡ‚Π΅ Π΄Π°Π½Π½ΠΈ: - Π‘Π΅Π· Ρ€Π΅ΠΊΠ»Π°ΠΌΠΈ, спСциални ΠΈΠ»ΠΈ ΠΏΠ»Π°Ρ‚Π΅Π½ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ. Всичко Π΅ напълно Π±Π΅Π·ΠΏΠ»Π°Ρ‚Π½ΠΎ. - Π‘Π΅Π· ΡˆΠΏΠΈΠΎΠ½ΠΈΡ€Π°Π½Π΅, слСдСнС, тСлСмСтрия ΠΈ ΠΎΡ‚Ρ‡Π΅Ρ‚ΠΈ. Π‘Π΅Π· глупости! -- Π‘Π΅Π· Π²Ρ€ΡŠΠ·ΠΊΠ° към ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚, освСн ΠΊΠΎΠ³Π°Ρ‚ΠΎ Π΅ Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎ гласовото въвСТданС. +- Използва ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚ само ΠΏΡ€ΠΈ Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎ гласово въвСТданС ΠΈ Π·Π° изтСглянС Π½Π° Ρ€Π΅Ρ‡Π½ΠΈΡ†ΠΈ ΠΎΡ‚ Github. ΠœΠΎΠΆΠ΅Ρ‚Π΅ Π΄Π° ΠΈΠ·Π±Π΅Ρ€Π΅Ρ‚Π΅ ΠΈ ΠΏΡŠΠ»Π½Π°Ρ‚Π° вСрсия, която ΡΡŠΠ΄ΡŠΡ€ΠΆΠ° всички Π΅Π·ΠΈΡ†ΠΈ ΠΈ Π½Π΅ изисква Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅Ρ‚ΠΎ Π·Π° ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚. - ЕдинствСно си Π²ΡŠΡ€ΡˆΠΈ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚Π°. - Π‘ ΠΎΡ‚Π²ΠΎΡ€Π΅Π½ ΠΊΠΎΠ΄, Ρ‚Π°ΠΊΠ° Ρ‡Π΅ ΠΌΠΎΠΆΠ΅ Π΄Π° ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚Π΅ Π³ΠΎΡ€Π½ΠΎΡ‚ΠΎ ΠΈ сами. - БъздадСна с ΠΏΠΎΠΌΠΎΡ‰Ρ‚Π° Π½Π° цялата общност. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 04047369..7865f843 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -5,7 +5,7 @@ Supported languages: Arabic, Bulgarian, Croatian, Czech, Danish, Dutch, English, Privacy Policy and Philosophy: - No ads, no premium or paid features. It's all free. - No spying, no tracking, no telemetry or reports. No nothing! -- No network connectivity, except when voice input is active. +- Network connectivity is only used for voice input and downloading dictionaries from Github. You can also use the Full version that includes all languages and requires no Internet permission. - It only does its job. - Open-source, so you can verify all the above yourself. -- Created with help from the entire community. \ No newline at end of file +- Created with help from the entire community.