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 38c81f60f..426392a0e 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.SharedPreferences; 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; @@ -29,9 +30,7 @@ import org.dslul.openboard.inputmethod.latin.utils.BinaryDictionaryUtils; import org.dslul.openboard.inputmethod.latin.utils.DictionaryInfoUtils; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.BufferUnderflowException; import java.util.ArrayList; import java.util.HashMap; @@ -64,7 +63,6 @@ final public class BinaryDictionaryGetter { public static final String MAIN_DICTIONARY_CATEGORY = "main"; public static final String ID_CATEGORY_SEPARATOR = ":"; - public static final String MAIN_DICTIONARY_FILE_NAME = MAIN_DICTIONARY_CATEGORY + ".dict"; public static final String ASSETS_DICTIONARY_FOLDER = "dicts"; // The key considered to read the version attribute in a dictionary file. @@ -319,23 +317,13 @@ final public class BinaryDictionaryGetter { if (bestMatchName == null) return null; // we have a match, now copy contents of the dictionary to "cached" word lists folder - File outfile = new File(DictionaryInfoUtils.getWordListCacheDirectory(context) + - File.separator + extractLocaleFromAssetsDictionaryFile(bestMatchName) + File.separator + - BinaryDictionaryGetter.MAIN_DICTIONARY_FILE_NAME); - File parentFile = outfile.getParentFile(); - if (parentFile == null || (!parentFile.exists() && !parentFile.mkdirs())) { - return null; - } + File dictFile = new File(DictionaryInfoUtils.getCacheDirectoryForLocale(bestMatchName, context) + + File.separator + DictionaryInfoUtils.MAIN_DICTIONARY_INTERNAL_FILE_NAME); try { - InputStream in = context.getAssets().open(ASSETS_DICTIONARY_FOLDER + File.separator + bestMatchName); - 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(); - return outfile; + 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; 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 class FileUtils { 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/CorrectionSettingsFragment.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/CorrectionSettingsFragment.java index 3ba15d1a6..afc437348 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -16,24 +16,34 @@ package org.dslul.openboard.inputmethod.latin.settings; -import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Bundle; import android.preference.Preference; -import android.preference.SwitchPreference; -import android.text.TextUtils; +import android.text.Html; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; +import android.widget.Toast; +import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants; import org.dslul.openboard.inputmethod.latin.R; -import org.dslul.openboard.inputmethod.latin.permissions.PermissionsManager; -import org.dslul.openboard.inputmethod.latin.permissions.PermissionsUtil; +import org.dslul.openboard.inputmethod.latin.common.FileUtils; +import org.dslul.openboard.inputmethod.latin.makedict.DictionaryHeader; import org.dslul.openboard.inputmethod.latin.userdictionary.UserDictionaryList; import org.dslul.openboard.inputmethod.latin.userdictionary.UserDictionarySettings; +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.TreeSet; /** @@ -55,6 +65,8 @@ public final class CorrectionSettingsFragment extends SubScreenFragment private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false; private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS; + private static final int DICTIONARY_REQUEST_CODE = 96834; + private static final String DICTIONARY_URL = "https://github.com/openboard-team/openboard/"; // TODO: update once it exists @Override public void onCreate(final Bundle icicle) { @@ -73,6 +85,19 @@ public final class CorrectionSettingsFragment extends SubScreenFragment if (ri == null) { overwriteUserDictionaryPreference(editPersonalDictionary); } + + // Ideally this would go to a preference screen where extra dictionaries can be managed + // so user can check which dictionaries exists (internal and added), and also delete them. + // But for now just adding new ones and replacing is ok. + final Preference addDictionary = findPreference(Settings.PREF_ADD_DICTIONARY); + if (addDictionary != null) + addDictionary.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + showAddDictionaryDialog(); + return true; + } + }); } private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { @@ -99,4 +124,111 @@ public final class CorrectionSettingsFragment extends SubScreenFragment } } + private void showAddDictionaryDialog() { + final String link = "" + + getResources().getString(R.string.dictionary_selection_link_text) + ""; + final Spanned message = Html.fromHtml(getResources().getString(R.string.dictionary_selection_message, link)); + final AlertDialog dialog = new AlertDialog.Builder( + DialogUtils.getPlatformDialogThemeContext(getActivity())) + .setTitle(R.string.dictionary_selection_title) + .setMessage(message) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.dictionary_selection_load_file, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/octet-stream"); + startActivityForResult(intent, DICTIONARY_REQUEST_CODE); + } + }) + .create(); + dialog.show(); + // make links in the HTML text work + ((TextView) dialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + } + + private void onDictionaryFileSelected(int resultCode, Intent resultData) { + if (resultCode != Activity.RESULT_OK || resultData == null) { + onDictionaryLoadingError(R.string.dictionary_selection_error); + return; + } + + final Uri uri = resultData.getData(); + if (uri == null) { + onDictionaryLoadingError(R.string.dictionary_selection_error); + return; + } + + final File cachedDictionaryFile = new File(getActivity().getCacheDir().getPath() + File.separator + "temp_dict"); + try { + FileUtils.copyStreamToNewFile( + getActivity().getContentResolver().openInputStream(uri), + cachedDictionaryFile); + } catch (IOException e) { + onDictionaryLoadingError(R.string.dictionary_selection_error); + return; + } + + final DictionaryHeader newHeader = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(cachedDictionaryFile, 0, cachedDictionaryFile.length()); + if (newHeader == null) { + cachedDictionaryFile.delete(); + onDictionaryLoadingError(R.string.dictionary_selection_file_error); + return; + } + + final String dictFolder = + DictionaryInfoUtils.getCacheDirectoryForLocale(newHeader.mLocaleString, getActivity()); + final File dictFile = new File(dictFolder + File.separator + DictionaryInfoUtils.MAIN_DICTIONARY_USER_FILE_NAME); + if (dictFile.exists()) { + final DictionaryHeader oldHeader = + DictionaryInfoUtils.getDictionaryFileHeaderOrNull(dictFile, 0, dictFile.length()); + if (oldHeader != null + && Integer.parseInt(oldHeader.mVersionString) > Integer.parseInt(newHeader.mVersionString) + && !shouldReplaceExistingUserDictionary()) { + cachedDictionaryFile.delete(); + return; + } + } + + if (!cachedDictionaryFile.renameTo(dictFile)) { + cachedDictionaryFile.delete(); + onDictionaryLoadingError(R.string.dictionary_selection_error); + return; + } + + // success, now remove internal dictionary file if it exists + final File internalDictFile = new File(dictFolder + File.separator + + DictionaryInfoUtils.MAIN_DICTIONARY_INTERNAL_FILE_NAME); + if (internalDictFile.exists()) + internalDictFile.delete(); + + // inform user about success + final String successMessageForLocale = getResources() + .getString(R.string.dictionary_selection_load_success, newHeader.mLocaleString); + Toast.makeText(getActivity(), successMessageForLocale, Toast.LENGTH_SHORT).show(); + + // inform LatinIME about new dictionary + final Intent newDictBroadcast = new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); + getActivity().sendBroadcast(newDictBroadcast); + } + + private void onDictionaryLoadingError(int resId) { + // show error message... maybe better as dialog so user definitely notices? + Toast.makeText(getActivity(), resId, Toast.LENGTH_LONG).show(); + } + + private boolean shouldReplaceExistingUserDictionary() { + // TODO: show dialog, ask user whether existing file should be replaced + // return true if yes, no otherwise (set .setCancelable(false) to avoid dismissing without the buttons!) + return true; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if (requestCode == DICTIONARY_REQUEST_CODE) + onDictionaryFileSelected(resultCode, resultData); + } + } 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 24990a334..35fd5f931 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"; // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead. public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE = "auto_correction_threshold"; 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 f22d2fb3c..338cb2a68 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 @@ -54,6 +54,8 @@ public class DictionaryInfoUtils { 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 MAIN_DICTIONARY_INTERNAL_FILE_NAME = DEFAULT_MAIN_DICT + ".dict"; + public static final String MAIN_DICTIONARY_USER_FILE_NAME = MAIN_DICT_PREFIX + "user.dict"; 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; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e0ccc187..19517ee73 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -461,6 +461,20 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM Add-on dictionaries Settings for dictionaries + + "Choose dictionary file" + + "Select a dictionary to replace the main dictionary of the same locale. Dictionaries can be downloaded at %s." + + "the project repository" + + "Load dictionary" + + "Dictionary for locale \"%s\" added" + + "Error: Selected file is not a valid dictionary file" + + "Error loading dictionary file" User dictionaries diff --git a/app/src/main/res/xml/prefs_screen_correction.xml b/app/src/main/res/xml/prefs_screen_correction.xml index 350a8951a..f366d4367 100644 --- a/app/src/main/res/xml/prefs_screen_correction.xml +++ b/app/src/main/res/xml/prefs_screen_correction.xml @@ -24,6 +24,10 @@ + +