allow users to select dictionaries

https://github.com/openboard-team/openboard/pull/569
https://github.com/openboard-team/openboard/pull/578
modified so dictionaries use the correct type instead of always main
This commit is contained in:
Helium314 2023-06-26 13:25:32 +02:00
parent 62e55fd70b
commit 29c252066d
62 changed files with 502 additions and 16 deletions

View file

@ -9,8 +9,8 @@ Plan / to do:
* ~upgrade dependencies~ * ~upgrade dependencies~
* upgrade NDK, https://github.com/openboard-team/openboard/issues/782 * upgrade NDK, https://github.com/openboard-team/openboard/issues/782
* maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid * maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid
* user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578 * ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~
* make additional dictionaries available for download, and link from app * make additional dictionaries available for download (from OpenBoard PRs)
* multi-lingual typing, https://github.com/openboard-team/openboard/pull/593 * multi-lingual typing, https://github.com/openboard-team/openboard/pull/593
* suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660 * suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660
* improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576 * improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576
@ -23,6 +23,10 @@ Plan / to do:
Changes: Changes:
* Updated dependencies * Updated dependencies
* Debug version can be installed along OpenBoard * Debug version can be installed along OpenBoard
* Allow users to add and replace built-in dictionaries
* modified / improved from https://github.com/openboard-team/openboard/pull/569 and https://github.com/openboard-team/openboard/pull/578
* dictionaries are available at https://github.com/Helium314/openboard/dictionaries/dict
* dictionary files starting with "main_" replace the built-in dictionary for the language, all other names work as add-on dictionaries
----- -----

View file

@ -42,7 +42,8 @@ android {
ndkVersion '23.2.8568313' ndkVersion '23.2.8568313'
androidResources { androidResources {
noCompress 'dict' noCompress 'main.dict'
noCompress 'empty.dict'
} }
} }

View file

@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import android.content.res.AssetFileDescriptor; import android.content.res.AssetFileDescriptor;
import android.util.Log; import android.util.Log;
import org.dslul.openboard.inputmethod.latin.common.FileUtils;
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils; import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants; import org.dslul.openboard.inputmethod.latin.define.DecoderSpecificConstants;
import org.dslul.openboard.inputmethod.latin.makedict.DictionaryHeader; import org.dslul.openboard.inputmethod.latin.makedict.DictionaryHeader;
@ -62,6 +63,8 @@ final public class BinaryDictionaryGetter {
public static final String MAIN_DICTIONARY_CATEGORY = "main"; public static final String MAIN_DICTIONARY_CATEGORY = "main";
public static final String ID_CATEGORY_SEPARATOR = ":"; public static final String ID_CATEGORY_SEPARATOR = ":";
public static final String ASSETS_DICTIONARY_FOLDER = "dicts";
// The key considered to read the version attribute in a dictionary file. // The key considered to read the version attribute in a dictionary file.
private static String VERSION_KEY = "version"; private static String VERSION_KEY = "version";
@ -170,8 +173,8 @@ final public class BinaryDictionaryGetter {
for (File directory : directoryList) { for (File directory : directoryList) {
if (!directory.isDirectory()) continue; if (!directory.isDirectory()) continue;
final String dirLocale = final String dirLocale =
DictionaryInfoUtils.getWordListIdFromFileName(directory.getName()); DictionaryInfoUtils.getWordListIdFromFileName(directory.getName()).toLowerCase(Locale.ENGLISH);
final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale); final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale.toLowerCase(Locale.ENGLISH));
if (LocaleUtils.isMatch(matchLevel)) { if (LocaleUtils.isMatch(matchLevel)) {
final File[] wordLists = directory.listFiles(); final File[] wordLists = directory.listFiles();
if (null != wordLists) { if (null != wordLists) {
@ -265,9 +268,16 @@ final public class BinaryDictionaryGetter {
} }
if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) { if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
final File dict = loadDictionaryFromAssets(locale.toString(), context);
final AssetFileAddress fallbackAsset;
if (dict == null) {
// fall back to the old way (maybe remove? will not work if files are compressed)
final int fallbackResId = final int fallbackResId =
DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale); DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId); fallbackAsset = loadFallbackResource(context, fallbackResId);
} else {
fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath());
}
if (null != fallbackAsset) { if (null != fallbackAsset) {
fileList.add(fallbackAsset); fileList.add(fallbackAsset);
} }
@ -275,4 +285,75 @@ final public class BinaryDictionaryGetter {
return fileList; return fileList;
} }
/**
* Returns the best matching main dictionary from assets.
*
* Actually copies the dictionary to cache folder, and then returns that file. This allows
* the dictionaries to be stored in a compressed way, reducing APK size.
* On next load, the dictionary in cache folder is found by getCachedWordLists
*
* Returns null on IO errors or if no matching dictionary is found
*/
public static File loadDictionaryFromAssets(final String locale, final Context context) {
final String[] dictionaryList = getAssetsDictionaryList(context);
if (null == dictionaryList) return null;
String bestMatchName = null;
int bestMatchLevel = 0;
for (String dictionary : dictionaryList) {
final String dictLocale =
extractLocaleFromAssetsDictionaryFile(dictionary);
if (dictLocale == null) continue;
// assets files may contain the locale in lowercase, but dictionary headers usually
// have an upper case country code, so we compare lowercase here
final int matchLevel = LocaleUtils.getMatchLevel(dictLocale.toLowerCase(Locale.ENGLISH), locale.toLowerCase(Locale.ENGLISH));
if (LocaleUtils.isMatch(matchLevel) && matchLevel > bestMatchLevel) {
bestMatchName = dictionary;
}
}
if (bestMatchName == null) return null;
// we have a match, now copy contents of the dictionary to cached word lists folder
final String bestMatchLocale = extractLocaleFromAssetsDictionaryFile(bestMatchName);
if (bestMatchLocale == null) return null;
File dictFile = new File(DictionaryInfoUtils.getCacheDirectoryForLocale(bestMatchLocale, context) +
File.separator + DictionaryInfoUtils.getMainDictFilename(bestMatchLocale));
try {
FileUtils.copyStreamToNewFile(
context.getAssets().open(ASSETS_DICTIONARY_FOLDER + File.separator + bestMatchName),
dictFile);
return dictFile;
} catch (IOException e) {
Log.e(TAG, "exception while looking for locale " + locale, e);
return null;
}
}
/**
* Returns the locale for a dictionary file name stored in assets.
*
* Assumes file name main_[locale].dict
*
* Returns the locale, or null if file name does not match the pattern
*/
public static String extractLocaleFromAssetsDictionaryFile(final String dictionaryFileName) {
if (dictionaryFileName.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX)
&& dictionaryFileName.endsWith(".dict")) {
return dictionaryFileName.substring(
DictionaryInfoUtils.MAIN_DICT_PREFIX.length(),
dictionaryFileName.lastIndexOf('.')
);
}
return null;
}
public static String[] getAssetsDictionaryList(final Context context) {
final String[] dictionaryList;
try {
dictionaryList = context.getAssets().list(ASSETS_DICTIONARY_FOLDER);
} catch (IOException e) {
return null;
}
return dictionaryList;
}
} }

View file

@ -17,7 +17,10 @@
package org.dslul.openboard.inputmethod.latin.common; package org.dslul.openboard.inputmethod.latin.common;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
/** /**
* A simple class to help with removing directories recursively. * A simple class to help with removing directories recursively.
@ -58,4 +61,19 @@ public class FileUtils {
toFile.delete(); toFile.delete();
return fromFile.renameTo(toFile); return fromFile.renameTo(toFile);
} }
public static void copyStreamToNewFile(InputStream in, File outfile) throws IOException {
File parentFile = outfile.getParentFile();
if (parentFile == null || (!parentFile.exists() && !parentFile.mkdirs())) {
throw new IOException("could not create parent folder");
}
FileOutputStream out = new FileOutputStream(outfile);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.flush();
}
} }

View file

@ -0,0 +1,329 @@
package org.dslul.openboard.inputmethod.latin.settings
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.preference.Preference
import android.text.Html
import android.text.method.LinkMovementMethod
import android.util.TypedValue
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import android.widget.Toast
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryGetter
import org.dslul.openboard.inputmethod.latin.R
import org.dslul.openboard.inputmethod.latin.common.FileUtils
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils
import org.dslul.openboard.inputmethod.latin.utils.DialogUtils
import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils
import java.io.File
import java.io.IOException
import java.util.*
class DictionarySettingsFragment : SubScreenFragment() {
// dict for which dialog is currently open (if any)
private var currentDictLocale: Locale? = null
private var currentDictState: Int? = null
private val cachedDictionaryFile by lazy { File(activity.cacheDir.path + File.separator + "temp_dict") }
private val currentDictExistsForUser get() = currentDictState == DICT_INTERNAL_AND_USER || currentDictState == DICT_USER_ONLY
private val currentDictExistsInternal get() = currentDictState == DICT_INTERNAL_AND_USER || currentDictState == DICT_INTERNAL_ONLY
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.additional_subtype_settings)
reloadDictionaries()
// + button to add dictionary
setHasOptionsMenu(true)
activity.actionBar?.setTitle(R.string.dictionary_settings_category)
}
// shows existing dictionaries as preferences
private fun reloadDictionaries() {
val screen = preferenceScreen ?: return
screen.removeAll()
val userDicts = mutableSetOf<Locale>()
val internalDicts = mutableSetOf<Locale>()
// get available dictionaries
// cached (internal in use and user dicts)
DictionaryInfoUtils.getCachedDirectoryList(activity)?.forEach { dir ->
if (!dir.isDirectory)
return@forEach
dir.list()?.forEach {
if (it.endsWith(USER_DICTIONARY_SUFFIX))
userDicts.add(dir.name.toLocale())
else if (it.startsWith(DictionaryInfoUtils.MAIN_DICT_PREFIX))
internalDicts.add(dir.name.toLocale())
}
}
// internal only
BinaryDictionaryGetter.getAssetsDictionaryList(activity)?.forEach { dictFile ->
BinaryDictionaryGetter.extractLocaleFromAssetsDictionaryFile(dictFile)?.let {
internalDicts.add(it.toLocale())
}
}
// first show user-added dictionaries
userDicts.sortedBy { it.displayName() }.forEach { dict ->
val pref = Preference(activity).apply {
title = dict.displayName()
setSummary(R.string.user_dictionary_summary)
setOnPreferenceClickListener {
// open dialog for update or delete / reset
currentDictLocale = dict
currentDictState = if (internalDicts.contains(dict)) DICT_INTERNAL_AND_USER else DICT_USER_ONLY
showUpdateDialog()
true
}
}
screen.addPreference(pref)
}
// TODO: only show if language is actually used?
internalDicts.sortedBy { it.displayName() }.forEach { dict ->
if (userDicts.contains(dict)) return@forEach // don't show a second time
val pref = Preference(activity).apply {
title = dict.displayName()
setSummary(R.string.internal_dictionary_summary)
setOnPreferenceClickListener {
// open dialog for update, maybe disabling if i can make it work?
currentDictLocale = dict
currentDictState = DICT_INTERNAL_ONLY
showUpdateDialog()
true
}
}
screen.addPreference(pref)
}
}
private fun showUpdateDialog() {
// -1: adding new dict, don't know where it may exist
// 0: user only -> offer delete
// 1: internal only -> only update (and maybe later: disable)
// 2: user and internal -> offer reset to internal
if (currentDictState == null) return
if (currentDictLocale == null && currentDictState != DICT_NEW)
return
val link = "<a href='$DICTIONARY_URL'>" +
resources.getString(R.string.dictionary_link_text) + "</a>"
val message = if (currentDictState == DICT_NEW)
Html.fromHtml(resources.getString(R.string.add_new_dictionary, link))
else
Html.fromHtml(resources.getString(R.string.update_dictionary, link))
val title = if (currentDictState == DICT_NEW) R.string.add_new_dictionary_title
else R.string.dictionary_settings_category
val updateButtonTitle = if (currentDictExistsForUser) R.string.update_dictionary_button
else R.string.user_dict_settings_add_menu_title
val builder = AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(activity))
.setNegativeButton(R.string.cancel, null)
.setMessage(message)
.setTitle(title)
.setPositiveButton(updateButtonTitle) { _, _ ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/octet-stream")
startActivityForResult(intent, DICTIONARY_REQUEST_CODE)
}
// allow removing dictionaries
if (currentDictExistsForUser) {
builder.setNeutralButton(if (currentDictExistsInternal) R.string.reset_dictionary else R.string.delete_dict) { _, _ ->
AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(activity))
.setTitle(R.string.remove_dictionary_title)
.setMessage(resources.getString(R.string.remove_dictionary_message, currentDictLocale?.displayName()))
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_dict) { _,_ ->
currentDictLocale?.getUserDictFilenames()?.let { files ->
var parent: File? = null
files.forEach {
val f = File(it)
parent = f.parentFile
f.delete()
}
if (parent?.list()?.isEmpty() == true)
parent?.delete()
}
reloadDictionaries()
}
.show()
}
}
val dialog = builder.create()
dialog.show()
// make links in the HTML text work
(dialog.findViewById<View>(android.R.id.message) as TextView).movementMethod =
LinkMovementMethod.getInstance()
}
// copied from CustomInputStyleSettingsFragment
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.add_style, menu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val value = TypedValue()
activity.theme.resolveAttribute(android.R.attr.colorForeground, value, true)
menu.findItem(R.id.action_add_style).icon?.setTint(value.data)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val itemId = item.itemId
if (itemId == R.id.action_add_style) {
currentDictLocale = null
currentDictState = DICT_NEW
showUpdateDialog()
}
return super.onOptionsItemSelected(item)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == DICTIONARY_REQUEST_CODE) onDictionaryFileSelected(resultCode, resultData)
}
private fun onDictionaryFileSelected(resultCode: Int, resultData: Intent?) {
if (resultCode != Activity.RESULT_OK || resultData == null) {
onDictionaryLoadingError(R.string.dictionary_load_error.resString())
return
}
val uri = resultData.data ?: return onDictionaryLoadingError(R.string.dictionary_load_error.resString())
cachedDictionaryFile.delete()
try {
FileUtils.copyStreamToNewFile(
activity.contentResolver.openInputStream(uri),
cachedDictionaryFile
)
} catch (e: IOException) {
onDictionaryLoadingError(R.string.dictionary_load_error.resString())
return
}
val newHeader = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(cachedDictionaryFile, 0, cachedDictionaryFile.length())
if (newHeader == null) {
cachedDictionaryFile.delete()
onDictionaryLoadingError(R.string.dictionary_file_error.resString())
return
}
val locale = newHeader.mLocaleString.toLocale()
if (currentDictLocale != null && locale != currentDictLocale) {
cachedDictionaryFile.delete()
onDictionaryLoadingError(resources.getString(R.string.dictionary_file_wrong_locale, locale.displayName(), currentDictLocale?.displayName()))
return
}
// idString is content of 'dictionary' key, in format <type>:<locale>
val dictionaryType = newHeader.mIdString.substringBefore(":")
val userDictFile = File(locale.getUserDictFilename(dictionaryType))
// ask for user confirmation if it would be a version downgrade or if user pressed add new,
// but we already have a user dictionary for the same locale
val shouldAskMessageId = if (userDictFile.exists()) {
val oldHeader = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(userDictFile, 0, userDictFile.length())
if (oldHeader != null && oldHeader.mVersionString.toInt() > newHeader.mVersionString.toInt())
R.string.overwrite_old_dicitonary_messsage
else if (currentDictState == DICT_NEW && currentDictLocale == null)
R.string.replace_dictionary_message
else 0
} else 0
if (shouldAskMessageId != 0)
showConfirmReplaceDialog(locale, dictionaryType, shouldAskMessageId)
else
moveCachedFileToDictionaries(locale, dictionaryType)
}
private fun showConfirmReplaceDialog(locale: Locale, dictionaryType: String, messageId: Int) {
AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(activity))
.setTitle(R.string.replace_dictionary)
.setMessage(resources.getString(messageId, locale.displayName()))
.setCancelable(false)
.setNegativeButton(R.string.cancel, ) { _,_ ->
cachedDictionaryFile.delete()
}
.setPositiveButton(R.string.replace_dictionary) { _,_ ->
moveCachedFileToDictionaries(locale, dictionaryType)
}
.show()
}
private fun moveCachedFileToDictionaries(locale: Locale, dictionaryType: String) {
val dictFile = File(locale.getUserDictFilename(dictionaryType))
if (!cachedDictionaryFile.renameTo(dictFile)) {
cachedDictionaryFile.delete()
onDictionaryLoadingError(R.string.dictionary_load_error.resString())
return
}
// success, now remove internal dictionary file if a main dictionary was added
if (dictionaryType == DictionaryInfoUtils.DEFAULT_MAIN_DICT)
File(locale.getInternalDictFilename()).delete()
// inform user about success
val successMessageForLocale = resources
.getString(R.string.dictionary_load_success, locale.displayName())
Toast.makeText(activity, successMessageForLocale, Toast.LENGTH_LONG).show()
// inform LatinIME about new dictionary
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
activity.sendBroadcast(newDictBroadcast)
reloadDictionaries()
}
private fun onDictionaryLoadingError(message: String) {
AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(activity))
.setNegativeButton(android.R.string.ok, null)
.setMessage(message)
.setTitle("loading error")
.show()
}
private fun Locale.getUserDictFilename(dictionaryType: String) =
DictionaryInfoUtils.getCacheDirectoryForLocale(this.toString(), activity) + File.separator + dictionaryType + "_" + USER_DICTIONARY_SUFFIX
private fun Locale.getUserDictFilenames(): List<String> {
val dicts = mutableListOf<String>()
val p = DictionaryInfoUtils.getCacheDirectoryForLocale(this.toString(), activity)
DictionaryInfoUtils.getCachedDirectoryList(activity)?.forEach { dir ->
if (!dir.isDirectory)
return@forEach
dir.list()?.forEach {
if (it.endsWith(USER_DICTIONARY_SUFFIX))
dicts.add(p + File.separator + it)
}
}
return dicts
}
private fun Locale.getInternalDictFilename() =
DictionaryInfoUtils.getCacheDirectoryForLocale(this.toString(), activity) + File.separator + DictionaryInfoUtils.getMainDictFilename(this.toString())
private fun String.displayName() = LocaleUtils.constructLocaleFromString(this).displayName()
private fun String.toLocale() = LocaleUtils.constructLocaleFromString(this)
private fun Locale.displayName() = getDisplayName(resources.configuration.locale)
private fun Int.resString() = resources.getString(this)
companion object {
private const val DICTIONARY_REQUEST_CODE = 96834
private const val DICTIONARY_URL =
"https://github.com/Helium314/openboard/dictionaries/dict"
private const val USER_DICTIONARY_SUFFIX = "user.dict"
private const val DICT_INTERNAL_AND_USER = 2
private const val DICT_INTERNAL_ONLY = 1
private const val DICT_USER_ONLY = 0
private const val DICT_NEW = -1
}
}

View file

@ -65,6 +65,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
public static final String PREF_CLIPBOARD_CLIPBOARD_KEY = "pref_clipboard_clipboard_key"; public static final String PREF_CLIPBOARD_CLIPBOARD_KEY = "pref_clipboard_clipboard_key";
public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
public static final String PREF_ADD_DICTIONARY = "add_dictionary";
public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction"; public static final String PREF_AUTO_CORRECTION = "pref_key_auto_correction";
public static final String PREF_AUTO_CORRECTION_CONFIDENCE = "pref_key_auto_correction_confidence"; public static final String PREF_AUTO_CORRECTION_CONFIDENCE = "pref_key_auto_correction_confidence";
// PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead. // PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead.

View file

@ -52,8 +52,9 @@ import javax.annotation.Nullable;
public class DictionaryInfoUtils { public class DictionaryInfoUtils {
private static final String TAG = DictionaryInfoUtils.class.getSimpleName(); private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
private static final String DEFAULT_MAIN_DICT = "main"; public static final String DEFAULT_MAIN_DICT = "main";
private static final String MAIN_DICT_PREFIX = "main_"; public static final String MAIN_DICT_PREFIX = DEFAULT_MAIN_DICT + "_";
private static final String DICTIONARY_CATEGORY_SEPARATOR_EXPRESSION = "[" + BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + "_]";
private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX; private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX;
// 6 digits - unicode is limited to 21 bits // 6 digits - unicode is limited to 21 bits
private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
@ -151,7 +152,7 @@ public class DictionaryInfoUtils {
/** /**
* Helper method to get the top level cache directory. * Helper method to get the top level cache directory.
*/ */
private static String getWordListCacheDirectory(final Context context) { public static String getWordListCacheDirectory(final Context context) {
return context.getFilesDir() + File.separator + "dicts"; return context.getFilesDir() + File.separator + "dicts";
} }
@ -212,10 +213,12 @@ public class DictionaryInfoUtils {
@Nullable @Nullable
public static String getCategoryFromFileName(@Nonnull final String fileName) { public static String getCategoryFromFileName(@Nonnull final String fileName) {
final String id = getWordListIdFromFileName(fileName); final String id = getWordListIdFromFileName(fileName);
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); final String[] idArray = id.split(DICTIONARY_CATEGORY_SEPARATOR_EXPRESSION);
// An id is supposed to be in format category:locale, so splitting on the separator // An id is supposed to be in format category:locale, so splitting on the separator
// should yield a 2-elements array // should yield a 2-elements array
if (2 != idArray.length) { // Also allow '_' as separator, this is ok for locales like pt_br because
// we're interested in the part before first separator anyway
if (1 == idArray.length) {
return null; return null;
} }
return idArray[0]; return idArray[0];
@ -225,7 +228,7 @@ public class DictionaryInfoUtils {
* Find out the cache directory associated with a specific locale. * Find out the cache directory associated with a specific locale.
*/ */
public static String getCacheDirectoryForLocale(final String locale, final Context context) { public static String getCacheDirectoryForLocale(final String locale, final Context context) {
final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale); final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale).toLowerCase(Locale.ENGLISH);
final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ relativeDirectoryName; + relativeDirectoryName;
final File directory = new File(absoluteDirectoryName); final File directory = new File(absoluteDirectoryName);
@ -238,10 +241,12 @@ public class DictionaryInfoUtils {
} }
public static boolean isMainWordListId(final String id) { public static boolean isMainWordListId(final String id) {
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR); final String[] idArray = id.split(DICTIONARY_CATEGORY_SEPARATOR_EXPRESSION);
// An id is supposed to be in format category:locale, so splitting on the separator // An id is supposed to be in format category:locale, so splitting on the separator
// should yield a 2-elements array // should yield a 2-elements array
if (2 != idArray.length) { // Also allow '_' as separator, this is ok for locales like pt_br because
// we're interested in the part before first separator anyway
if (1 == idArray.length) {
return false; return false;
} }
return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]); return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
@ -318,6 +323,10 @@ public class DictionaryInfoUtils {
BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.toString().toLowerCase(); BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.toString().toLowerCase();
} }
public static String getMainDictFilename(@Nonnull final String locale) {
return MAIN_DICT_PREFIX + locale.toLowerCase(Locale.ENGLISH) + ".dict";
}
public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file,
final long offset, final long length) { final long offset, final long length) {
try { try {

View file

@ -467,6 +467,44 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<string name="dictionary_settings_title">Add-on dictionaries</string> <string name="dictionary_settings_title">Add-on dictionaries</string>
<!-- Title for the prompt dialog which informs the user that a dictionary is available for the current language and asks to decide whether to download it over 3g --> <!-- Title for the prompt dialog which informs the user that a dictionary is available for the current language and asks to decide whether to download it over 3g -->
<string name="dictionary_settings_summary">Settings for dictionaries</string> <string name="dictionary_settings_summary">Settings for dictionaries</string>
<!-- Title for dictionaries preference screen -->
<string name="dictionary_settings_category">Dictionaries</string>
<!-- Summary text for dictionary added by user -->
<string name="user_dictionary_summary">User-added dictionary</string>
<!-- Summary text for built-in dictionary -->
<string name="internal_dictionary_summary">Built-in dictionary</string>
<!-- Title for the adding new user dictionary dialog -->
<string name="add_new_dictionary_title">"Add dictionary from file"</string>
<!-- Button text for updating a dictionary -->
<string name="update_dictionary_button">"Update"</string>
<!-- Message for user dictionary replacement dialog -->
<string name="replace_dictionary_message">"Really replace existing user-added dictionary for %s?"</string>
<!-- Message when trying to replace user dictionary with older version -->
<string name="overwrite_old_dicitonary_messsage">"New dictionary file contains older version code than current file. Really replace current dictionary for %s?"</string>
<!-- Title and confirm button text for user dictionary replacement dialog -->
<string name="replace_dictionary">"Replace dictionary"</string>
<!-- Message for user dictionary remove dialog -->
<string name="remove_dictionary_message">"Really remove user-added dictionaries for %s?"</string>
<!-- Title for user dictionary remove dialog -->
<string name="remove_dictionary_title">"Remove dictionary"</string>
<!-- Button text in user dictionary remove dialog for resetting to default -->
<string name="reset_dictionary">"Reset to default"</string>
<!-- Message for the user dictionary selection dialog. This string will be interpreted as HTML -->
<string name="update_dictionary">"Select a dictionary to replace the current main dictionary. Dictionaries can be downloaded at %s."</string>
<!-- Message for the user dictionary selection dialog. This string will be interpreted as HTML -->
<string name="add_new_dictionary">"Select a new dictionary to be added to the list. Dictionaries can be downloaded at the %s."</string>
<!-- Title of the link to the download page inserted into selection message (above) -->
<string name="dictionary_link_text">"project repository"</string>
<!-- Button text for dictionary file selection -->
<string name="load_dictionary_file">"Load dictionary"</string>
<!-- Toast text shown when dictionary file for a locale was added successfully -->
<string name="dictionary_load_success">"Dictionary for %s added"</string>
<!-- Text shown when dictionary file could not be read -->
<string name="dictionary_file_error">"Error: Selected file is not a valid dictionary file"</string>
<!-- Text shown when dictionary file is not for the selected locale -->
<string name="dictionary_file_wrong_locale">"Error: Selected file is for %1$s, but %2$s was expected"</string>
<!-- Text shown on other errors when loading dictionary file -->
<string name="dictionary_load_error">"Error loading dictionary file"</string>
<!-- Name of the user dictionaries settings category --> <!-- Name of the user dictionaries settings category -->
<string name="user_dictionaries">User dictionaries</string> <string name="user_dictionaries">User dictionaries</string>
<!-- Name for the "user dictionary" preference item when there is only one --> <!-- Name for the "user dictionary" preference item when there is only one -->

View file

@ -36,6 +36,11 @@
android:title="@string/settings_screen_correction" android:title="@string/settings_screen_correction"
android:key="screen_correction" android:key="screen_correction"
android:icon="@drawable/ic_settings_correction"/> android:icon="@drawable/ic_settings_correction"/>
<PreferenceScreen
android:fragment="org.dslul.openboard.inputmethod.latin.settings.DictionarySettingsFragment"
android:title="@string/dictionary_settings_category"
android:key="add_dictionary"
android:icon="@drawable/ic_notify_dictionary"/>
<PreferenceScreen <PreferenceScreen
android:fragment="org.dslul.openboard.inputmethod.latin.settings.AdvancedSettingsFragment" android:fragment="org.dslul.openboard.inputmethod.latin.settings.AdvancedSettingsFragment"
android:title="@string/settings_screen_advanced" android:title="@string/settings_screen_advanced"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.