diff --git a/android/app/build.gradle b/android/app/build.gradle index ad7cfecc98..b92bc5c2b9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -115,9 +115,6 @@ android { // but continue the build even when errors are found: abortOnError false } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - } // https://developer.android.com/about/versions/marshmallow/android-6.0-changes#behavior-apache-http-client useLibrary 'org.apache.http.legacy' @@ -125,7 +122,8 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - //testImplementation 'junit:junit:4.12' - //androidTestImplementation 'com.android.support.test:runner:1.0.2' - //androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support:support-annotations:28.0.0' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } diff --git a/android/app/src/androidTest/AndroidManifest.xml b/android/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..9ff0d4b8d8 --- /dev/null +++ b/android/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/app/src/androidTest/java/org/coolreader/crengine/FindFcLangCodeTest.java b/android/app/src/androidTest/java/org/coolreader/crengine/FindFcLangCodeTest.java new file mode 100644 index 0000000000..90c4ecc443 --- /dev/null +++ b/android/app/src/androidTest/java/org/coolreader/crengine/FindFcLangCodeTest.java @@ -0,0 +1,126 @@ +package org.coolreader.crengine; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class FindFcLangCodeTest { + @Test + public void testFindCompatibleFcLangCode() + { + // English codes variants + String langCode = Engine.findCompatibleFcLangCode("English"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("english"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("eng"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("Eng"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en-US"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("eng-US"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en-USA"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("eng-USA"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en-GB"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("eng-GB"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en-GBR"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("eng-GBR"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en_gb"); + assertEquals("en", langCode); + + langCode = Engine.findCompatibleFcLangCode("en"); + assertEquals("en", langCode); + + // Russian codes variants + langCode = Engine.findCompatibleFcLangCode("Russian"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("russian"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("Rus"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("rus"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("ru-RU"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("rus-RU"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("ru-RUS"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("rus-RUS"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("ru_ru"); + assertEquals("ru", langCode); + + langCode = Engine.findCompatibleFcLangCode("ru"); + assertEquals("ru", langCode); + + // Chinese variants + langCode = Engine.findCompatibleFcLangCode("zh-CN"); + assertEquals("zh_cn", langCode); + + langCode = Engine.findCompatibleFcLangCode("zho-CN"); + assertEquals("zh_cn", langCode); + + langCode = Engine.findCompatibleFcLangCode("zh-CHN"); + assertEquals("zh_cn", langCode); + + langCode = Engine.findCompatibleFcLangCode("zho-CHN"); + assertEquals("zh_cn", langCode); + + langCode = Engine.findCompatibleFcLangCode("zh-HK"); + assertEquals("zh_hk", langCode); + + langCode = Engine.findCompatibleFcLangCode("zho-HK"); + assertEquals("zh_hk", langCode); + + langCode = Engine.findCompatibleFcLangCode("zh-HKG"); + assertEquals("zh_hk", langCode); + + langCode = Engine.findCompatibleFcLangCode("zho-HKG"); + assertEquals("zh_hk", langCode); + + langCode = Engine.findCompatibleFcLangCode("zh_hk"); + assertEquals("zh_hk", langCode); + + // invalid variant + langCode = Engine.findCompatibleFcLangCode("abcd"); + assertNull(langCode); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b6bec4a94c..111144ce2a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,8 +13,6 @@ android:anyDensity="true" /> - - diff --git a/android/gradle.properties b/android/gradle.properties index aac7c9b461..14a6522047 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -15,3 +15,7 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + +# When set to all, summary or none, Gradle will use different warning type display. +# See Command-line logging options for details. +org.gradle.warning.mode=all diff --git a/android/jni/cr3engine.cpp b/android/jni/cr3engine.cpp index 8141e4c028..3791c86f8c 100644 --- a/android/jni/cr3engine.cpp +++ b/android/jni/cr3engine.cpp @@ -24,6 +24,8 @@ #include <../../crengine/include/fb2def.h> +#include "fc-lang-cat.h" + #define XS_IMPLEMENT_SCHEME 1 #include <../../crengine/include/fb2def.h> #include @@ -657,6 +659,33 @@ JNIEXPORT jboolean JNICALL Java_org_coolreader_crengine_Engine_setCacheDirectory return res ? JNI_TRUE : JNI_FALSE; } +/* + * Class: org_coolreader_crengine_Engine + * Method: haveFcLangCodeInternal + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_org_coolreader_crengine_Engine_haveFcLangCodeInternal + (JNIEnv *env, jclass cls, jstring langCode) +{ + jboolean res = JNI_FALSE; + const char* langCode_ptr = env->GetStringUTFChars(langCode, 0); + if (langCode_ptr) { + struct fc_lang_catalog* lang_ptr = fc_lang_cat; + for (int i = 0; i < fc_lang_cat_sz; i++) + { + if (strcmp(lang_ptr->lang_code, langCode_ptr) == 0) + { + res = JNI_TRUE; + break; + } + lang_ptr++; + } + env->ReleaseStringUTFChars(langCode, langCode_ptr); + } + return res; +} + + /* * Class: org_coolreader_crengine_Engine * Method: checkFontLanguageCompatibilityInternal diff --git a/android/jni/org_coolreader_crengine_Engine.h b/android/jni/org_coolreader_crengine_Engine.h index 44d1c1dd9c..c294706fda 100644 --- a/android/jni/org_coolreader_crengine_Engine.h +++ b/android/jni/org_coolreader_crengine_Engine.h @@ -107,6 +107,14 @@ JNIEXPORT void JNICALL Java_org_coolreader_crengine_Engine_drawBookCoverInternal JNIEXPORT void JNICALL Java_org_coolreader_crengine_Engine_suspendLongOperationInternal (JNIEnv *, jclass); +/* + * Class: org_coolreader_crengine_Engine + * Method: haveFcLangCodeInternal + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_org_coolreader_crengine_Engine_haveFcLangCodeInternal + (JNIEnv *, jclass, jstring); + /* * Class: org_coolreader_crengine_Engine * Method: checkFontLanguageCompatibilityInternal diff --git a/android/res/values-ru/strings.xml b/android/res/values-ru/strings.xml index bb803b70a0..0518b8b9a9 100644 --- a/android/res/values-ru/strings.xml +++ b/android/res/values-ru/strings.xml @@ -591,7 +591,7 @@ Контрастная черная Ошибка открытия документа \"%s\" Каталог с данными приложения \"%s\" был удален другой программой!\nВозможно какой-то оптимизатор дискового пространства?\nК сожалению, все настройки утеряны. - "Шрифт \"%s\" не совместим с языком \"%s\". Вместо него будет задействован дополнительный шрифт." + "Предупреждение: шрифт \"%s\" не совместим с языком \"%s\". Вместо него будет задействован дополнительный шрифт." Набор символов различных писменностей от Fontconfig: https://www.fontconfig.org/ diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index e730216da4..8c0ff1b489 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -617,7 +617,7 @@ "20 pages (track)" Error while opening document \"%s\" Application data directory \"%s\" has been removed by other app.\nPossibly it was some kind of disc space optimizer.\nUnfortunately all the settings were lost. - "Font \"%s\" isn't compatible with language \"%s\". Instead will be used fallback font." + "Notice: font \"%s\" isn't compatible with language \"%s\". Instead will be used fallback font." "Languages character set database by Fontconfig: https://www.fontconfig.org/ " diff --git a/android/src/org/coolreader/crengine/Engine.java b/android/src/org/coolreader/crengine/Engine.java index 5f029c08e4..1cf94806d8 100644 --- a/android/src/org/coolreader/crengine/Engine.java +++ b/android/src/org/coolreader/crengine/Engine.java @@ -15,6 +15,7 @@ import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; +import java.util.MissingResourceException; import java.util.zip.ZipEntry; import org.coolreader.R; @@ -593,9 +594,24 @@ public void initAgain() { private native static void suspendLongOperationInternal(); // cancel current long operation in engine thread (swapping to cache file) -- call it from GUI thread + /** + * Test if in embedded FontConfig language orthography catalog have record with language code langCode. + * @param langCode language code + * @return true if record with langCode found, false - otherwise. + * + * Language code compared as is without any modifications. + */ + private native static boolean haveFcLangCodeInternal(String langCode); + + /** + * Check the font for compatibility with the specified language. + * @param fontFace font face to check. + * @param langCode language code in embedded FontConfig language orthography catalog. + * @return true if font compatible with language, false - otherwise. + */ private native static boolean checkFontLanguageCompatibilityInternal(String fontFace, String langCode); - - public static void suspendLongOperation() { + + public static void suspendLongOperation() { suspendLongOperationInternal(); } @@ -604,6 +620,137 @@ public synchronized static boolean checkFontLanguageCompatibility(String fontFac return checkFontLanguageCompatibilityInternal(fontFace, langCode); } + /** + * Finds the corresponding language code in embedded FontConfig language orthography catalog. + * @param language language code in free form: ISO 639-1, ISO 639-2 or full name of the language in English. Also allowed concatenation of country code in ISO 3166-1 alpha-2 or ISO 3166-1 alpha-3. + * @return language code in the FontConfig language orthography catalog if it's found, null - otherwise. + * + * If a country code in any form is added to the language, but the record with the country code is not found - it is simply ignored and the search continues without a country code. + */ + public static String findCompatibleFcLangCode(String language) + { + String langCode = null; + + String lang_part; + String country_part; + String testLang; + + // Split language and country codes + int pos = language.indexOf('-'); + if (-1 == pos) + pos = language.indexOf('_'); + if (pos > 0) { + lang_part = language.substring(0, pos); + if (pos < language.length() - 1) + country_part = language.substring(pos + 1); + else + country_part = ""; + } else { + lang_part = language; + country_part = ""; + } + lang_part = lang_part.toLowerCase(); + country_part = country_part.toLowerCase(); + + if (country_part.length() > 0) + testLang = lang_part + "_" + country_part; + else + testLang = lang_part; + // 1. Check if testLang is already language code accepted by FontConfig languages symbols database + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + else { + // Check if lang_part is the three-letter abbreviation: ISO 639-2 or ISO 639-3 + // and if country_part code is the three-letter country code: ISO 3366-1 alpha 3 + // Then convert them to two-letter code and test + String lang_2l = null; + String country_2l = null; + int found = 0; + for (Locale loc : Locale.getAvailableLocales()) { + try { + if (lang_part.equals(loc.getISO3Language())) { + lang_2l = loc.getLanguage(); + found |= 1; + } + } catch (MissingResourceException e) { + // three-letter language abbreviation is not available for this locale + // just ignore this exception + } + if (country_part.length() > 0) { + try { + if (country_part.equals(loc.getISO3Country().toLowerCase())) { + country_2l = loc.getCountry().toLowerCase(); + found |= 2; + } + } catch (MissingResourceException e) { + // the three-letter country abbreviation is not available for this locale + // just ignore this exception + } + if (3 == found) + break; + } else if (1 == found) + break; + } + if (country_part.length() > 0) { + // 2. test lang_2l + country_part + if (null != lang_2l) { + testLang = lang_2l + "_" + country_part; + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } + if (null == langCode && null != country_2l) { + // 3. test lang_part + country_2l + testLang = lang_part + "_" + country_2l; + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } + if (null == langCode && null != country_2l && null != lang_2l) { + // 4. test lang_2l + country_2l + testLang = lang_2l + "_" + country_2l; + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } + } + if (null == langCode) + { + if (null != lang_2l) { + // 5. test lang_2l + // if two-letter country code not found or county code omitted + // but found two-letter language code + testLang = lang_2l; + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } else { + // 6. test lang_part + testLang = lang_part; + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } + } + } + if (null == langCode) { + Locale locale = null; + // 7. Try to find by full language name + for (Locale loc : Locale.getAvailableLocales()) { + if (language.equalsIgnoreCase(loc.getDisplayLanguage(Locale.ENGLISH))) { + locale = loc; + break; + } + } + if (null != locale) { + testLang = locale.getISO3Language(); + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + else { + testLang = locale.getLanguage(); // two-letter code + if (haveFcLangCodeInternal(testLang)) + langCode = testLang; + } + } + } + return langCode; + } + /** * Checks whether specified directlry or file is symbolic link. * (thread-safe) diff --git a/android/src/org/coolreader/crengine/ReaderView.java b/android/src/org/coolreader/crengine/ReaderView.java index c2222c33f4..d9dad09431 100644 --- a/android/src/org/coolreader/crengine/ReaderView.java +++ b/android/src/org/coolreader/crengine/ReaderView.java @@ -2579,9 +2579,12 @@ private void applySettings(Properties props) FileInfo fileInfo = mBookInfo.getFileInfo(); final String bookLanguage = fileInfo.getLanguage(); final String fontFace = props.getProperty(PROP_FONT_FACE); - if (null != bookLanguage && bookLanguage.length() > 0) { - boolean res = Engine.checkFontLanguageCompatibility(fontFace, bookLanguage); - log.d("Checking font \"" + fontFace + "\" for compatibility with language \"" + bookLanguage + "\": res=" + res); + String fcLangCode = null; + if (null != bookLanguage && bookLanguage.length() > 0) + fcLangCode = Engine.findCompatibleFcLangCode(bookLanguage); + if (null != fcLangCode && fcLangCode.length() > 0) { + boolean res = Engine.checkFontLanguageCompatibility(fontFace, fcLangCode); + log.d("Checking font \"" + fontFace + "\" for compatibility with language \"" + bookLanguage + "\" fcLangCode=" + fcLangCode + ": res=" + res); if (!res) { BackgroundThread.instance().executeGUI(new Runnable() { @Override @@ -2591,7 +2594,8 @@ public void run() { }); } } else { - log.d("Can't get book's language to check font compatibility! bookInfo=" + fileInfo); + if (null != bookLanguage) + log.d("Can't find compatible language code in embedded FontConfig catalog: language=\"" + bookLanguage + "\" bookInfo=" + fileInfo); } } doc.applySettings(props);