1
0
Fork 0

'full' and 'lite' flavors

This commit is contained in:
sspanak 2024-06-12 12:48:55 +03:00 committed by Dimo Karaivanov
parent 11e042e707
commit 6670bccc50
16 changed files with 182 additions and 64 deletions

View file

@ -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

2
.gitignore vendored
View file

@ -16,6 +16,8 @@ gen/
.gradle/
assets/
build/
full/
lite/
release/
# Local configuration file (sdk path, etc)

View file

@ -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.

View file

@ -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"
}
}
}

View file

@ -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")

View file

@ -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}"
}
}
}

View file

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <!-- allows dictionary download -->
</manifest>

View file

@ -68,7 +68,7 @@ public class DictionaryLoader {
}
public boolean load(ArrayList<Language> languages) {
public boolean load(Context context, ArrayList<Language> 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<Language>() {{ add(language); }});
self.load(context, new ArrayList<Language>() {{ 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();

View file

@ -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());
}
}
}

View file

@ -70,7 +70,7 @@ class ItemLoadDictionary extends ItemClickable {
ArrayList<Language> languages = LanguageCollection.getAll(activity, activity.getSettings().getEnabledLanguageIds());
setLoadingStatus();
if (!loader.load(languages)) {
if (!loader.load(activity, languages)) {
loader.stop();
setReadyStatus();
}

View file

@ -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

View file

@ -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 {

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string translatable="false" name="dictionary_url">https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s</string>
<string translatable="false" name="help_url">https://github.com/sspanak/tt9/blob/master/docs/user-manual.md</string>
<string name="app_name" translatable="false">Traditional T9</string>
<string name="app_name_short" translatable="false">TT9</string>
@ -80,6 +81,7 @@
<string name="dictionary_cancel_load">Cancel Loading</string>
<string name="dictionary_load_bad_char">Loading failed. Invalid word \"%1$s\" on line %2$d of language \"%3$s\".</string>
<string name="dictionary_load_error">Failed loading the dictionary for language \"%1$s\" (%2$s).</string>
<string name="dictionary_load_no_internet">Failed downloading the dictionary for language \"%1$s\". Check your Internet connection.</string>
<string name="dictionary_load_cancelled">Dictionary load cancelled.</string>
<string name="dictionary_loaded">Dictionary load completed.</string>
<string name="dictionary_loading">Loading dictionary (%1$s)…</string>

View file

@ -44,6 +44,10 @@ def generateVersionName() {
return "$versionTagsCount.$commitsSinceLastTag$betaString"
}
ext.exec = { command ->
return execThing(command)
}
ext.getVersionName = { ->
return generateVersionName()
}

View file

@ -1,11 +1,11 @@
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текста на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
Traditional T9 е 12-клавишна клавиатура за устройства с копчета. Поддържа подскаващ текст на повече от 25 езика и бързи клавиши, а виртуалната клавиатура пресъздава доброто старо усещане за Нокия на съвременните телефони със сензорен екран. И най-хубавото е, че не ви шпионира.
Поддържани езици: арабски, български, хърватски, чешки, датски, холандски, английски, финландски, френски, немски, гръцки, иврит, унгарски, индонезийски, италиански, кисуахили, норвежки, полски, португалски (европейски и бразилски), румънски, руски, испански, шведски, турски, украински, идиш.
Философия и защита не личните данни:
- Без реклами, специални или платени функции. Всичко е напълно безплатно.
- Без шпиониране, следене, телеметрия и отчети. Без глупости!
- Без връзка към интернет, освен когато е активно гласовото въвеждане.
- Използва интернет само при активно гласово въвеждане и за изтегляне на речници от Github. Можете да изберете и пълната версия, която съдържа всички езици и не изисква разрешението за интернет.
- Единствено си върши работата.
- С отворен код, така че може да проверите горното и сами.
- Създадена с помощта на цялата общност.

View file

@ -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.
- Created with help from the entire community.