diff --git a/README.md b/README.md index 2303e9b48..dd343c029 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Plan / to do: * ~upgrade dependencies~ * 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 -* user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578 - * make additional dictionaries available for download, and link from app +* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~ + * make additional dictionaries available for download (from OpenBoard PRs) * 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 * improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576 @@ -23,6 +23,10 @@ Plan / to do: Changes: * Updated dependencies * 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 ----- diff --git a/app/build.gradle b/app/build.gradle index fd4394c37..b8714ed03 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,7 +42,8 @@ android { ndkVersion '23.2.8568313' androidResources { - noCompress 'dict' + noCompress 'main.dict' + noCompress 'empty.dict' } } diff --git a/app/src/main/res/raw/main_bg.dict b/app/src/main/assets/dicts/main_bg.dict similarity index 100% rename from app/src/main/res/raw/main_bg.dict rename to app/src/main/assets/dicts/main_bg.dict diff --git a/app/src/main/res/raw/main_bn.dict b/app/src/main/assets/dicts/main_bn.dict similarity index 100% rename from app/src/main/res/raw/main_bn.dict rename to app/src/main/assets/dicts/main_bn.dict diff --git a/app/src/main/res/raw/main_da.dict b/app/src/main/assets/dicts/main_da.dict similarity index 100% rename from app/src/main/res/raw/main_da.dict rename to app/src/main/assets/dicts/main_da.dict diff --git a/app/src/main/res/raw/main_de.dict b/app/src/main/assets/dicts/main_de.dict similarity index 100% rename from app/src/main/res/raw/main_de.dict rename to app/src/main/assets/dicts/main_de.dict diff --git a/app/src/main/res/raw/main_el.dict b/app/src/main/assets/dicts/main_el.dict similarity index 100% rename from app/src/main/res/raw/main_el.dict rename to app/src/main/assets/dicts/main_el.dict diff --git a/app/src/main/res/raw/main_en.dict b/app/src/main/assets/dicts/main_en.dict similarity index 100% rename from app/src/main/res/raw/main_en.dict rename to app/src/main/assets/dicts/main_en.dict diff --git a/app/src/main/res/raw/main_eo.dict b/app/src/main/assets/dicts/main_eo.dict similarity index 100% rename from app/src/main/res/raw/main_eo.dict rename to app/src/main/assets/dicts/main_eo.dict diff --git a/app/src/main/res/raw/main_es.dict b/app/src/main/assets/dicts/main_es.dict similarity index 100% rename from app/src/main/res/raw/main_es.dict rename to app/src/main/assets/dicts/main_es.dict diff --git a/app/src/main/res/raw/main_fr.dict b/app/src/main/assets/dicts/main_fr.dict similarity index 100% rename from app/src/main/res/raw/main_fr.dict rename to app/src/main/assets/dicts/main_fr.dict diff --git a/app/src/main/res/raw/main_hu.dict b/app/src/main/assets/dicts/main_hu.dict similarity index 100% rename from app/src/main/res/raw/main_hu.dict rename to app/src/main/assets/dicts/main_hu.dict diff --git a/app/src/main/res/raw/main_it.dict b/app/src/main/assets/dicts/main_it.dict similarity index 100% rename from app/src/main/res/raw/main_it.dict rename to app/src/main/assets/dicts/main_it.dict diff --git a/app/src/main/res/raw/main_nl.dict b/app/src/main/assets/dicts/main_nl.dict similarity index 100% rename from app/src/main/res/raw/main_nl.dict rename to app/src/main/assets/dicts/main_nl.dict diff --git a/app/src/main/res/raw/main_pl.dict b/app/src/main/assets/dicts/main_pl.dict similarity index 100% rename from app/src/main/res/raw/main_pl.dict rename to app/src/main/assets/dicts/main_pl.dict diff --git a/app/src/main/res/raw/main_pt_br.dict b/app/src/main/assets/dicts/main_pt_br.dict similarity index 100% rename from app/src/main/res/raw/main_pt_br.dict rename to app/src/main/assets/dicts/main_pt_br.dict diff --git a/app/src/main/res/raw/main_pt_pt.dict b/app/src/main/assets/dicts/main_pt_pt.dict similarity index 100% rename from app/src/main/res/raw/main_pt_pt.dict rename to app/src/main/assets/dicts/main_pt_pt.dict diff --git a/app/src/main/res/raw/main_ro.dict b/app/src/main/assets/dicts/main_ro.dict similarity index 100% rename from app/src/main/res/raw/main_ro.dict rename to app/src/main/assets/dicts/main_ro.dict diff --git a/app/src/main/res/raw/main_ru.dict b/app/src/main/assets/dicts/main_ru.dict similarity index 100% rename from app/src/main/res/raw/main_ru.dict rename to app/src/main/assets/dicts/main_ru.dict diff --git a/app/src/main/res/raw/main_sv.dict b/app/src/main/assets/dicts/main_sv.dict similarity index 100% rename from app/src/main/res/raw/main_sv.dict rename to app/src/main/assets/dicts/main_sv.dict diff --git a/app/src/main/res/raw/main_tr.dict b/app/src/main/assets/dicts/main_tr.dict similarity index 100% rename from app/src/main/res/raw/main_tr.dict rename to app/src/main/assets/dicts/main_tr.dict diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java index 08c7026db..164c95786 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/BinaryDictionaryGetter.java @@ -21,6 +21,7 @@ import android.content.res.AssetFileDescriptor; 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.define.DecoderSpecificConstants; 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 ID_CATEGORY_SEPARATOR = ":"; + public static final String ASSETS_DICTIONARY_FOLDER = "dicts"; + // The key considered to read the version attribute in a dictionary file. private static String VERSION_KEY = "version"; @@ -170,8 +173,8 @@ public static File[] getCachedWordLists(final String locale, final Context conte for (File directory : directoryList) { if (!directory.isDirectory()) continue; final String dirLocale = - DictionaryInfoUtils.getWordListIdFromFileName(directory.getName()); - final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale); + DictionaryInfoUtils.getWordListIdFromFileName(directory.getName()).toLowerCase(Locale.ENGLISH); + final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale.toLowerCase(Locale.ENGLISH)); if (LocaleUtils.isMatch(matchLevel)) { final File[] wordLists = directory.listFiles(); if (null != wordLists) { @@ -265,9 +268,16 @@ public static ArrayList getDictionaryFiles(final Locale locale } if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) { - final int fallbackResId = - DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale); - final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId); + 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 = + DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale); + fallbackAsset = loadFallbackResource(context, fallbackResId); + } else { + fallbackAsset = AssetFileAddress.makeFromFileName(dict.getPath()); + } if (null != fallbackAsset) { fileList.add(fallbackAsset); } @@ -275,4 +285,75 @@ public static ArrayList getDictionaryFiles(final Locale locale 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; + } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/common/FileUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/common/FileUtils.java index fcbbadcd8..0dda3246a 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/common/FileUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/common/FileUtils.java @@ -17,7 +17,10 @@ package org.dslul.openboard.inputmethod.latin.common; import java.io.File; +import java.io.FileOutputStream; import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; /** * A simple class to help with removing directories recursively. @@ -58,4 +61,19 @@ public static boolean renameTo(final File fromFile, final File toFile) { toFile.delete(); 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(); + } + } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/DictionarySettingsFragment.kt b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/DictionarySettingsFragment.kt new file mode 100644 index 000000000..3397498d1 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/DictionarySettingsFragment.kt @@ -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() + val internalDicts = mutableSetOf() + // 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 = "" + + resources.getString(R.string.dictionary_link_text) + "" + 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(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 : + 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 { + val dicts = mutableListOf() + 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 + + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java index 52647b444..159b3dee2 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java @@ -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_CLIPBOARD_CLIPBOARD_KEY = "pref_clipboard_clipboard_key"; 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_CONFIDENCE = "pref_key_auto_correction_confidence"; // PREF_SHOW_SUGGESTIONS_SETTING_OBSOLETE is obsolete. Use PREF_SHOW_SUGGESTIONS instead. diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java index 364ecdf9d..44b368318 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -52,8 +52,9 @@ public class DictionaryInfoUtils { private static final String TAG = DictionaryInfoUtils.class.getSimpleName(); public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); - private static final String DEFAULT_MAIN_DICT = "main"; - private static final String MAIN_DICT_PREFIX = "main_"; + public static final String DEFAULT_MAIN_DICT = "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; // 6 digits - unicode is limited to 21 bits private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; @@ -151,7 +152,7 @@ public static String replaceFileNameDangerousCharacters(final String name) { /** * 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"; } @@ -212,10 +213,12 @@ public static File[] getStagingDirectoryList(final Context context) { @Nullable public static String getCategoryFromFileName(@Nonnull final String 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 // 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 idArray[0]; @@ -225,7 +228,7 @@ public static String getCategoryFromFileName(@Nonnull final String fileName) { * Find out the cache directory associated with a specific locale. */ 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 + relativeDirectoryName; final File directory = new File(absoluteDirectoryName); @@ -238,10 +241,12 @@ public static String getCacheDirectoryForLocale(final String locale, final Conte } 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 // 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 BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]); @@ -318,6 +323,10 @@ public static String getMainDictId(@Nonnull final Locale locale) { 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, final long offset, final long length) { try { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ef73fc30..00cbd741f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -467,6 +467,44 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM Add-on dictionaries Settings for dictionaries + + Dictionaries + + User-added dictionary + + Built-in dictionary + + "Add dictionary from file" + + "Update" + + "Really replace existing user-added dictionary for %s?" + + "New dictionary file contains older version code than current file. Really replace current dictionary for %s?" + + "Replace dictionary" + + "Really remove user-added dictionaries for %s?" + + "Remove dictionary" + + "Reset to default" + + "Select a dictionary to replace the current main dictionary. Dictionaries can be downloaded at %s." + + "Select a new dictionary to be added to the list. Dictionaries can be downloaded at the %s." + + "project repository" + + "Load dictionary" + + "Dictionary for %s added" + + "Error: Selected file is not a valid dictionary file" + + "Error: Selected file is for %1$s, but %2$s was expected" + + "Error loading dictionary file" User dictionaries diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml index f5f9f0495..a4cde530c 100644 --- a/app/src/main/res/xml/prefs.xml +++ b/app/src/main/res/xml/prefs.xml @@ -36,6 +36,11 @@ android:title="@string/settings_screen_correction" android:key="screen_correction" android:icon="@drawable/ic_settings_correction"/> +