allow users to add dictionaries

This commit is contained in:
Helium 2022-03-19 10:55:02 +01:00
parent 1f369ab791
commit 40c09e4b1e
7 changed files with 183 additions and 24 deletions

View file

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

View file

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

View file

@ -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 = "<a href='" + DICTIONARY_URL + "'>" +
getResources().getString(R.string.dictionary_selection_link_text) + "</a>";
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);
}
}

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_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";

View file

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

View file

@ -461,6 +461,20 @@ disposition rather than other common dispositions for Latin languages. [CHAR LIM
<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 -->
<string name="dictionary_settings_summary">Settings for dictionaries</string>
<!-- Title for the user dictionary selection dialog -->
<string name="dictionary_selection_title">"Choose dictionary file"</string>
<!-- Message for the user dictionary selection dialog. This string will be interpreted as HTML -->
<string name="dictionary_selection_message">"Select a dictionary to replace the main dictionary of the same locale. Dictionaries can be downloaded at %s."</string>
<!-- Title of the link to the download page inserted into selection message -->
<string name="dictionary_selection_link_text">"the project repository"</string>
<!-- Button text for dictionary file selection -->
<string name="dictionary_selection_load_file">"Load dictionary"</string>
<!-- Toast text shown when dictionary file was added successfully -->
<string name="dictionary_selection_load_success">"Dictionary for locale \"%s\" added"</string>
<!-- Text shown when dictionary file could not be read -->
<string name="dictionary_selection_file_error">"Error: Selected file is not a valid dictionary file"</string>
<!-- Text shown on other errors when loading dictionary file -->
<string name="dictionary_selection_error">"Error loading dictionary file"</string>
<!-- Name of the user dictionaries settings category -->
<string name="user_dictionaries">User dictionaries</string>
<!-- Name for the "user dictionary" preference item when there is only one -->

View file

@ -24,6 +24,10 @@
<intent android:action="android.settings.USER_DICTIONARY_SETTINGS" />
</PreferenceScreen>
<Preference
android:key="add_dictionary"
android:title="@string/configure_dictionaries_title" />
<PreferenceCategory
android:title="@string/settings_category_correction">