From 575293edb9e82da1962e39c8d0f1caa91a9beeb6 Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Fri, 14 Oct 2022 18:00:45 +0300 Subject: [PATCH] * each input mode is now in a separate file (#75) * fixed mode 123 being incorrectly forced after typing in a numeric field, then going to a text field * simplified context usage everywhere * added some missing translations * moved the Soft Key view to the SoftKey class to avoid memory leaks and to simplify the code a bit --- res/values-de/strings.xml | 1 + res/values-fr/strings.xml | 1 + res/values-it/strings.xml | 1 + res/values-uk/strings.xml | 1 + .../github/sspanak/tt9/db/DictionaryDb.java | 60 +-- .../sspanak/tt9/ime/InputFieldHelper.java | 16 +- .../sspanak/tt9/ime/InputModeValidator.java | 15 +- .../github/sspanak/tt9/ime/KeyPadHandler.java | 21 +- .../sspanak/tt9/ime/SoftKeyHandler.java | 44 ++- .../github/sspanak/tt9/ime/TraditionalT9.java | 360 ++++++------------ .../sspanak/tt9/ime/modes/InputMode.java | 77 ++++ .../github/sspanak/tt9/ime/modes/Mode123.java | 28 ++ .../github/sspanak/tt9/ime/modes/ModeABC.java | 38 ++ .../sspanak/tt9/ime/modes/ModePredictive.java | 196 ++++++++++ .../sspanak/tt9/languages/Language.java | 15 +- .../tt9/preferences/T9Preferences.java | 31 +- .../tt9/settings_legacy/SettingMultiList.java | 4 +- src/io/github/sspanak/tt9/ui/AddWordAct.java | 6 +- .../github/sspanak/tt9/ui/CandidateView.java | 6 +- .../sspanak/tt9/ui/TraditionalT9Settings.java | 52 +-- src/io/github/sspanak/tt9/ui/UI.java | 26 +- 21 files changed, 606 insertions(+), 393 deletions(-) create mode 100644 src/io/github/sspanak/tt9/ime/modes/InputMode.java create mode 100644 src/io/github/sspanak/tt9/ime/modes/Mode123.java create mode 100644 src/io/github/sspanak/tt9/ime/modes/ModeABC.java create mode 100644 src/io/github/sspanak/tt9/ime/modes/ModePredictive.java diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 9ced7d3b..45de385d 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -19,4 +19,5 @@ Lade Benutzerwörterbuch… Wörterbuch laden Wird nicht geladen. Wörterbuch für %1$s nicht gefunden. + Wörterbuch löschen diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 8ee0e4e0..b7d2f0a5 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -19,4 +19,5 @@ Chargement du dictionnaire utilisateur… Charger le dictionnaire Echec du chargement. Dictionnaire %1$s introuvable. + Supprimer le dictionaire diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index aeaaf8f2..f15cca57 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -19,5 +19,6 @@ Caricamento dizionario utente… Caricamento dizionario Impossibile caricare. Dizionario per %1$s non trovato. + Eliminare il dizionario diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 5b9281cc..245f06dc 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -19,4 +19,5 @@ Завантаження словника користувача… Завантажити словник Помилка завантаження. Словник %1$s не знайдено. + Очистити словник diff --git a/src/io/github/sspanak/tt9/db/DictionaryDb.java b/src/io/github/sspanak/tt9/db/DictionaryDb.java index a897a084..7ebd17c7 100644 --- a/src/io/github/sspanak/tt9/db/DictionaryDb.java +++ b/src/io/github/sspanak/tt9/db/DictionaryDb.java @@ -1,6 +1,5 @@ package io.github.sspanak.tt9.db; -import android.content.Context; import android.database.sqlite.SQLiteConstraintException; import android.os.Bundle; import android.os.Handler; @@ -15,6 +14,7 @@ import java.util.ArrayList; import java.util.List; import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.ime.TraditionalT9; import io.github.sspanak.tt9.languages.InvalidLanguageException; import io.github.sspanak.tt9.languages.Language; @@ -43,32 +43,32 @@ public class DictionaryDb { }; - private static synchronized void createInstance(Context context) { - dbInstance = Room.databaseBuilder(context, T9RoomDb.class, "t9dict.db") + private static synchronized void createInstance() { + dbInstance = Room.databaseBuilder(TraditionalT9.getMainContext(), T9RoomDb.class, "t9dict.db") .addCallback(TRIGGER_CALLBACK) .build(); } - public static T9RoomDb getInstance(Context context) { + private static T9RoomDb getInstance() { if (dbInstance == null) { - createInstance(context); + createInstance(); } return dbInstance; } - public static void beginTransaction(Context context) { - getInstance(context).beginTransaction(); + public static void beginTransaction() { + getInstance().beginTransaction(); } - public static void endTransaction(Context context, boolean success) { + public static void endTransaction(boolean success) { if (success) { - getInstance(context).setTransactionSuccessful(); + getInstance().setTransactionSuccessful(); } - getInstance(context).endTransaction(); + getInstance().endTransaction(); } @@ -81,18 +81,18 @@ public class DictionaryDb { } - public static void truncateWords(Context context, Handler handler) { + public static void truncateWords(Handler handler) { new Thread() { @Override public void run() { - getInstance(context).clearAllTables(); + getInstance().clearAllTables(); handler.sendEmptyMessage(0); } }.start(); } - public static void insertWord(Context context, Handler handler, Language language, String word) throws Exception { + public static void insertWord(Handler handler, Language language, String word) throws Exception { if (language == null) { throw new InvalidLanguageException(); } @@ -111,8 +111,8 @@ public class DictionaryDb { @Override public void run() { try { - getInstance(context).wordsDao().insert(dbWord); - getInstance(context).wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence); + getInstance().wordsDao().insert(dbWord); + getInstance().wordsDao().incrementFrequency(dbWord.langId, dbWord.word, dbWord.sequence); handler.sendEmptyMessage(0); } catch (SQLiteConstraintException e) { String msg = "Constraint violation when inserting a word: '" + dbWord.word + "' / sequence: '" + dbWord.sequence + "', for language: " + dbWord.langId; @@ -128,12 +128,12 @@ public class DictionaryDb { } - public static void insertWordsSync(Context context, List words) { - getInstance(context).wordsDao().insertMany(words); + public static void insertWordsSync(List words) { + getInstance().wordsDao().insertMany(words); } - public static void incrementWordFrequency(Context context, Language language, String word, String sequence) throws Exception { + public static void incrementWordFrequency(Language language, String word, String sequence) throws Exception { if (language == null) { throw new InvalidLanguageException(); } @@ -153,7 +153,7 @@ public class DictionaryDb { @Override public void run() { try { - getInstance(context).wordsDao().incrementFrequency(language.getId(), word, sequence); + getInstance().wordsDao().incrementFrequency(language.getId(), word, sequence); } catch (Exception e) { Logger.e( DictionaryDb.class.getName(), @@ -165,7 +165,7 @@ public class DictionaryDb { } - public static void getSuggestions(Context context, Handler handler, Language language, String sequence, int minimumWords, int maximumWords) { + public static void getSuggestions(Handler handler, Language language, String sequence, int minimumWords, int maximumWords) { final int minWords = Math.max(minimumWords, 0); final int maxWords = Math.max(maximumWords, minimumWords); @@ -173,24 +173,24 @@ public class DictionaryDb { @Override public void run() { if (sequence == null || sequence.length() == 0) { - Logger.w("tt9/getSuggestions", "Attempting to get suggestions for an empty sequence."); + Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for an empty sequence."); sendSuggestions(handler, new ArrayList<>()); return; } if (language == null) { - Logger.w("tt9/getSuggestions", "Attempting to get suggestions for NULL language."); + Logger.w("tt9/db.getSuggestions", "Attempting to get suggestions for NULL language."); sendSuggestions(handler, new ArrayList<>()); return; } // get exact sequence matches, for example: "9422" -> "what" - List exactMatches = getInstance(context).wordsDao().getMany(language.getId(), sequence, maxWords); - Logger.d("getWords", "Exact matches: " + exactMatches.size()); + List exactMatches = getInstance().wordsDao().getMany(language.getId(), sequence, maxWords); + Logger.d("db.getSuggestions", "Exact matches: " + exactMatches.size()); ArrayList suggestions = new ArrayList<>(); for (Word word : exactMatches) { - Logger.d("getWords", "exact match: " + word.word + ", priority: " + word.frequency); + Logger.d("db.getSuggestions", "exact match: " + word.word + ", priority: " + word.frequency); suggestions.add(word.word); } @@ -198,15 +198,19 @@ public class DictionaryDb { // for example: "rol" => "roll", "roller", "rolling", ... if (exactMatches.size() < minWords && sequence.length() >= 2) { int extraWordsNeeded = minWords - exactMatches.size(); - List extraWords = getInstance(context).wordsDao().getFuzzy(language.getId(), sequence, extraWordsNeeded); - Logger.d("getWords", "Fuzzy matches: " + extraWords.size()); + List extraWords = getInstance().wordsDao().getFuzzy(language.getId(), sequence, extraWordsNeeded); + Logger.d("db.getSuggestions", "Fuzzy matches: " + extraWords.size()); for (Word word : extraWords) { - Logger.d("getWords", "fuzzy match: " + word.word + ", sequence: " + word.sequence); + Logger.d("db.getSuggestions", "fuzzy match: " + word.word + ", sequence: " + word.sequence); suggestions.add(word.word); } } + if (suggestions.size() == 0) { + Logger.i("db.getSuggestions", "No suggestions for sequence: " + sequence); + } + // pack the words in a message and send it to the calling thread sendSuggestions(handler, suggestions); } diff --git a/src/io/github/sspanak/tt9/ime/InputFieldHelper.java b/src/io/github/sspanak/tt9/ime/InputFieldHelper.java index f35813c7..da4ed7a6 100644 --- a/src/io/github/sspanak/tt9/ime/InputFieldHelper.java +++ b/src/io/github/sspanak/tt9/ime/InputFieldHelper.java @@ -10,6 +10,8 @@ import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.github.sspanak.tt9.ime.modes.InputMode; + class InputFieldHelper { public static boolean isThereText(InputConnection currentInputConnection) { @@ -79,7 +81,7 @@ class InputFieldHelper { ArrayList allowedModes = new ArrayList<>(); if (inputField == null) { - allowedModes.add(TraditionalT9.MODE_123); + allowedModes.add(InputMode.MODE_123); return allowedModes; } @@ -90,8 +92,8 @@ class InputFieldHelper { && inputField.privateImeOptions.equals("io.github.sspanak.tt9.addword=true") ) ) { - allowedModes.add(TraditionalT9.MODE_123); - allowedModes.add(TraditionalT9.MODE_ABC); + allowedModes.add(InputMode.MODE_123); + allowedModes.add(InputMode.MODE_ABC); return allowedModes; } @@ -103,7 +105,7 @@ class InputFieldHelper { case InputType.TYPE_CLASS_PHONE: // Phones will also default to the symbols keyboard, though // often you will want to have a dedicated phone keyboard. - allowedModes.add(TraditionalT9.MODE_123); + allowedModes.add(InputMode.MODE_123); return allowedModes; case InputType.TYPE_CLASS_TEXT: @@ -112,7 +114,7 @@ class InputFieldHelper { // be doing predictive text (showing candidates as the // user types). if (!isSpecializedTextField(inputField)) { - allowedModes.add(TraditionalT9.MODE_PREDICTIVE); + allowedModes.add(InputMode.MODE_PREDICTIVE); } // ↓ fallthrough to add ABC and 123 modes ↓ @@ -120,8 +122,8 @@ class InputFieldHelper { default: // For all unknown input types, default to the alphabetic // keyboard with no special features. - allowedModes.add(TraditionalT9.MODE_123); - allowedModes.add(TraditionalT9.MODE_ABC); + allowedModes.add(InputMode.MODE_123); + allowedModes.add(InputMode.MODE_ABC); return allowedModes; } diff --git a/src/io/github/sspanak/tt9/ime/InputModeValidator.java b/src/io/github/sspanak/tt9/ime/InputModeValidator.java index 80be48b6..c129587a 100644 --- a/src/io/github/sspanak/tt9/ime/InputModeValidator.java +++ b/src/io/github/sspanak/tt9/ime/InputModeValidator.java @@ -3,9 +3,10 @@ package io.github.sspanak.tt9.ime; import java.util.ArrayList; import io.github.sspanak.tt9.Logger; -import io.github.sspanak.tt9.languages.definitions.English; +import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.languages.definitions.English; import io.github.sspanak.tt9.preferences.T9Preferences; public class InputModeValidator { @@ -42,16 +43,16 @@ public class InputModeValidator { return validLanguage; } - public static int validateMode(T9Preferences prefs, int inputMode, ArrayList allowedModes) { - if (allowedModes.size() > 0 && allowedModes.contains(inputMode)) { + public static InputMode validateMode(T9Preferences prefs, InputMode inputMode, ArrayList allowedModes) { + if (allowedModes.size() > 0 && allowedModes.contains(inputMode.getId())) { return inputMode; } - int newMode = allowedModes.size() > 0 ? allowedModes.get(0) : TraditionalT9.MODE_123; + InputMode newMode = InputMode.getInstance(allowedModes.size() > 0 ? allowedModes.get(0) : InputMode.MODE_123); prefs.saveInputMode(newMode); - if (newMode != inputMode) { - Logger.w("tt9/validateMode", "Invalid input mode: " + inputMode + " Enforcing: " + newMode); + if (newMode.getId() != inputMode.getId()) { + Logger.w("tt9/validateMode", "Invalid input mode: " + inputMode.getId() + " Enforcing: " + newMode.getId()); } return newMode; @@ -62,7 +63,7 @@ public class InputModeValidator { return textCase; } - int newCase = allowedTextCases.size() > 0 ? allowedTextCases.get(0) : TraditionalT9.CASE_LOWER; + int newCase = allowedTextCases.size() > 0 ? allowedTextCases.get(0) : InputMode.CASE_LOWER; prefs.saveTextCase(newCase); if (textCase != newCase) { diff --git a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java index d8517748..23a098c4 100644 --- a/src/io/github/sspanak/tt9/ime/KeyPadHandler.java +++ b/src/io/github/sspanak/tt9/ime/KeyPadHandler.java @@ -7,8 +7,8 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import io.github.sspanak.tt9.ui.CandidateView; import io.github.sspanak.tt9.preferences.T9Preferences; +import io.github.sspanak.tt9.ui.CandidateView; abstract class KeyPadHandler extends InputMethodService { @@ -28,7 +28,7 @@ abstract class KeyPadHandler extends InputMethodService { // temporal key handling private int ignoreNextKeyUp = 0; private int lastKeyCode = 0; - protected boolean isNumKeyRepeated = false; + private boolean isNumKeyRepeated = false; // throttling private static final int BACKSPACE_DEBOUNCE_TIME = 80; @@ -42,7 +42,7 @@ abstract class KeyPadHandler extends InputMethodService { @Override public void onCreate() { super.onCreate(); - prefs = T9Preferences.getInstance(this); + prefs = new T9Preferences(getApplicationContext()); onInit(); } @@ -95,8 +95,8 @@ abstract class KeyPadHandler extends InputMethodService { @Override public void onStartInput(EditorInfo inputField, boolean restarting) { currentInputConnection = getCurrentInputConnection(); - // Logger.d("T9.onStartInput", "INPUTTYPE: " + inputField.inputType + " FIELDID: " + inputField.fieldId + - // " FIELDNAME: " + inputField.fieldName + " PACKAGE NAME: " + inputField.packageName); + // Logger.d("T9.onStartInput", "inputType: " + inputField.inputType + " fieldId: " + inputField.fieldId + + // " fieldName: " + inputField.fieldName + " packageName: " + inputField.packageName); mEditing = NON_EDIT; @@ -219,7 +219,7 @@ abstract class KeyPadHandler extends InputMethodService { } switch (keyCode) { - case KeyEvent.KEYCODE_0: return on0(true); + case KeyEvent.KEYCODE_0: case KeyEvent.KEYCODE_1: case KeyEvent.KEYCODE_2: case KeyEvent.KEYCODE_3: @@ -229,7 +229,7 @@ abstract class KeyPadHandler extends InputMethodService { case KeyEvent.KEYCODE_7: case KeyEvent.KEYCODE_8: case KeyEvent.KEYCODE_9: - return on1to9(keyCodeToKeyNumber(keyCode), true); + return onNumber(keyCodeToKeyNumber(keyCode), true, false); } ignoreNextKeyUp = 0; @@ -275,7 +275,7 @@ abstract class KeyPadHandler extends InputMethodService { } if (keyCode == KeyEvent.KEYCODE_0) { - return on0(false); + return onNumber(0, false, isNumKeyRepeated); } // dialer fields are similar to pure numeric fields, but for user convenience, holding "0" @@ -305,7 +305,7 @@ abstract class KeyPadHandler extends InputMethodService { case KeyEvent.KEYCODE_7: case KeyEvent.KEYCODE_8: case KeyEvent.KEYCODE_9: - return on1to9(keyCodeToKeyNumber(keyCode), false); + return onNumber(keyCodeToKeyNumber(keyCode), false, isNumKeyRepeated); case KeyEvent.KEYCODE_STAR: return onStar(); case KeyEvent.KEYCODE_POUND: return onPound(); } @@ -378,8 +378,7 @@ abstract class KeyPadHandler extends InputMethodService { abstract public boolean onOK(); abstract protected boolean onUp(); abstract protected boolean onDown(); - abstract protected boolean on0(boolean hold); - abstract protected boolean on1to9(int key, boolean hold); + abstract protected boolean onNumber(int key, boolean hold, boolean repeat); abstract protected boolean onStar(); abstract protected boolean onPound(); diff --git a/src/io/github/sspanak/tt9/ime/SoftKeyHandler.java b/src/io/github/sspanak/tt9/ime/SoftKeyHandler.java index 274ad679..090f84e8 100644 --- a/src/io/github/sspanak/tt9/ime/SoftKeyHandler.java +++ b/src/io/github/sspanak/tt9/ime/SoftKeyHandler.java @@ -1,5 +1,6 @@ package io.github.sspanak.tt9.ime; +import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -8,18 +9,39 @@ import io.github.sspanak.tt9.ui.UI; class SoftKeyHandler implements View.OnTouchListener { private static final int[] buttons = { R.id.main_left, R.id.main_right, R.id.main_mid }; - private final TraditionalT9 parent; + private final TraditionalT9 tt9; + private View view = null; - public SoftKeyHandler(View mainView, TraditionalT9 tt9) { - this.parent = tt9; - changeView(mainView); + public SoftKeyHandler(LayoutInflater layoutInflater, TraditionalT9 tt9) { + this.tt9 = tt9; + + createView(layoutInflater); } - public void changeView(View v) { - for (int buttonId : buttons) { - View button = v.findViewById(buttonId); - button.setOnTouchListener(this); + View createView(LayoutInflater layoutInflater) { + if (view == null) { + view = layoutInflater.inflate(R.layout.mainview, null); + + for (int buttonId : buttons) { + view.findViewById(buttonId).setOnTouchListener(this); + } + } + + return view; + } + + + void show() { + if (view != null) { + view.setVisibility(View.VISIBLE); + } + } + + + void hide() { + if (view != null) { + view.setVisibility(View.GONE); } } @@ -30,17 +52,17 @@ class SoftKeyHandler implements View.OnTouchListener { int buttonId = view.getId(); if (buttonId == R.id.main_left && action == MotionEvent.ACTION_UP) { - UI.showPreferencesScreen(parent); + UI.showPreferencesScreen(tt9); return view.performClick(); } if (buttonId == R.id.main_mid && action == MotionEvent.ACTION_UP) { - parent.onOK(); + tt9.onOK(); return view.performClick(); } if (buttonId == R.id.main_right && action == MotionEvent.AXIS_PRESSURE) { - return parent.handleBackspaceHold(); + return tt9.handleBackspaceHold(); } return false; diff --git a/src/io/github/sspanak/tt9/ime/TraditionalT9.java b/src/io/github/sspanak/tt9/ime/TraditionalT9.java index 7c6cda70..5fbca7cf 100644 --- a/src/io/github/sspanak/tt9/ime/TraditionalT9.java +++ b/src/io/github/sspanak/tt9/ime/TraditionalT9.java @@ -1,5 +1,6 @@ package io.github.sspanak.tt9.ime; +import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -9,31 +10,24 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import io.github.sspanak.tt9.Logger; -import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryDb; +import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.LanguageCollection; -import io.github.sspanak.tt9.languages.Punctuation; import io.github.sspanak.tt9.ui.UI; public class TraditionalT9 extends KeyPadHandler { + private static TraditionalT9 self; + // input mode - public static final int MODE_PREDICTIVE = 0; - public static final int MODE_ABC = 1; - public static final int MODE_123 = 2; private ArrayList allowedInputModes = new ArrayList<>(); - private int mInputMode = MODE_123; + private InputMode mInputMode; // text case - public static final int CASE_LOWER = 0; - public static final int CASE_CAPITALIZE = 1; - public static final int CASE_UPPER = 2; private ArrayList allowedTextCases = new ArrayList<>(); - private int mTextCase = CASE_LOWER; + private int mTextCase = InputMode.CASE_LOWER; // language protected ArrayList mEnabledLanguages; @@ -41,16 +35,18 @@ public class TraditionalT9 extends KeyPadHandler { // soft key view private SoftKeyHandler softKeyHandler = null; - private View softKeyView = null; - // @todo: move predictive mode stuff in its own class in #66 - private String predictionSequence = ""; + + + public static Context getMainContext() { + return self.getApplicationContext(); + } private void loadPreferences() { mLanguage = LanguageCollection.getLanguage(prefs.getInputLanguage()); mEnabledLanguages = prefs.getEnabledLanguages(); - mInputMode = prefs.getInputMode(); + mInputMode = InputMode.getInstance(prefs.getInputMode()); mTextCase = prefs.getTextCase(); } @@ -67,8 +63,10 @@ public class TraditionalT9 extends KeyPadHandler { protected void onInit() { + self = this; + if (softKeyHandler == null) { - softKeyHandler = new SoftKeyHandler(getLayoutInflater().inflate(R.layout.mainview, null), this); + softKeyHandler = new SoftKeyHandler(getLayoutInflater(), this); } loadPreferences(); @@ -77,15 +75,18 @@ public class TraditionalT9 extends KeyPadHandler { protected void onRestart(EditorInfo inputField) { - // in case we are back from Preferences screen, update the language list - mEnabledLanguages = prefs.getEnabledLanguages(); - validatePreferences(); + // determine the valid state for the current input field and preferences + determineAllowedInputModes(inputField); + determineAllowedTextCases(); + mEnabledLanguages = prefs.getEnabledLanguages(); // in case we are back from Preferences screen, update the language list - // reset all UI elements - predictionSequence = ""; + // enforce a valid initial state + validatePreferences(); clearSuggestions(); + + // build the UI UI.updateStatusIcon(this, mLanguage, mInputMode, mTextCase); - displaySoftKeyMenu(); + softKeyHandler.show(); if (!isInputViewShown()) { showWindow(true); } @@ -93,37 +94,31 @@ public class TraditionalT9 extends KeyPadHandler { requestShowSelf(1); } - determineAllowedInputModes(inputField); - determineAllowedTextCases(); - restoreAddedWordIfAny(); } protected void onFinish() { - predictionSequence = ""; clearSuggestions(); hideStatusIcon(); hideWindow(); - if (softKeyView != null) { - softKeyView.setVisibility(View.GONE); - } + softKeyHandler.hide(); } public boolean onBackspace() { if (!InputFieldHelper.isThereText(currentInputConnection)) { Logger.d("onBackspace", "backspace ignored"); + mInputMode.reset(); return false; } resetKeyRepeat(); - if (mInputMode == MODE_PREDICTIVE && predictionSequence.length() > 1) { - predictionSequence = predictionSequence.substring(0, predictionSequence.length() - 1); - applyPredictionSequence(); + if (mInputMode.onBackspace()) { + getSuggestions(); } else { commitCurrentSuggestion(); super.sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); @@ -139,6 +134,7 @@ public class TraditionalT9 extends KeyPadHandler { acceptCurrentSuggestion(); resetKeyRepeat(); + mInputMode.reset(); return !isSuggestionViewHidden(); } @@ -153,72 +149,46 @@ public class TraditionalT9 extends KeyPadHandler { return nextSuggestion(); } + /** + * onNumber + * + * @param key Must be a number from 1 to 9, not a "KeyEvent.KEYCODE_X" + * @param hold If "true" we are calling the handler, because the key is being held. + * @param repeat If "true" we are calling the handler, because the key was pressed more than once + * @return boolean + */ + protected boolean onNumber(int key, boolean hold, boolean repeat) { + if (mInputMode.shouldAcceptCurrentSuggestion(key, hold, repeat)) { + acceptCurrentSuggestion(); + } - protected boolean on0(boolean hold) { - if (!hold && nextSuggestionInModeAbc()) { + if (!mInputMode.onNumber(mLanguage, key, hold, repeat)) { + return false; + } + + if (mInputMode.shouldSelectNextSuggestion() && !isSuggestionViewHidden()) { + nextSuggestion(); return true; } - acceptCurrentSuggestion(); - - setSuggestions( - mInputMode == MODE_ABC && !hold ? mLanguage.getKeyCharacters(0) : null, - 0 - ); - - if (hold) { - String chr = mInputMode == MODE_123 ? "+" : "0"; - currentInputConnection.commitText(chr, 1); - } else if (mInputMode == MODE_PREDICTIVE) { - currentInputConnection.commitText(" ", 1); - } else if (mInputMode == MODE_123) { - currentInputConnection.commitText("0", 1); + if (mInputMode.getWord() != null) { + setText(mInputMode.getWord()); + } else { + getSuggestions(); } return true; } - /** - * on1to9 - * - * @param key Must be a number from 1 to 9, not a "KeyEvent.KEYCODE_X" - * @param hold If "true" we are calling the handler, because the key is being held. - * @return boolean - */ - protected boolean on1to9(int key, boolean hold) { - if (mInputMode == MODE_123) { - return false; - } - - if (hold) { - commitCurrentSuggestion(); // commit the previous one before adding the "hold" character - currentInputConnection.commitText(String.valueOf(key), 1); - return true; - } else if (wordInPredictiveMode(key)) { - return true; - } else if (emoticonInPredictiveMode(key)) { - return true; - } else if (nextSuggestionInModeAbc()) { - return true; - } else if (mInputMode == MODE_ABC || mInputMode == MODE_PREDICTIVE) { - commitCurrentSuggestion(); // commit the previous one before suggesting the new one - setSuggestions(mLanguage.getKeyCharacters(key, mTextCase == CASE_LOWER)); - setComposingTextFromCurrentSuggestion(); - return true; - } - - return false; - } - protected boolean onPound() { - currentInputConnection.commitText("#", 1); + setText("#"); return true; } protected boolean onStar() { - currentInputConnection.commitText("*", 1); + setText("*"); return true; } @@ -231,7 +201,7 @@ public class TraditionalT9 extends KeyPadHandler { if (hold) { nextLang(); } else { - nextKeyMode(); + nextInputMode(); } return true; @@ -254,7 +224,7 @@ public class TraditionalT9 extends KeyPadHandler { protected boolean shouldTrackNumPress() { - return mInputMode != TraditionalT9.MODE_123; + return mInputMode.shouldTrackNumPress(); } @@ -274,9 +244,7 @@ public class TraditionalT9 extends KeyPadHandler { } mSuggestionView.scrollToSuggestion(-1); - - String word = mSuggestionView.getCurrentSuggestion(); - currentInputConnection.setComposingText(word, word.length()); + setComposingTextFromCurrentSuggestion(); return true; } @@ -288,143 +256,46 @@ public class TraditionalT9 extends KeyPadHandler { } mSuggestionView.scrollToSuggestion(1); - - String word = mSuggestionView.getCurrentSuggestion(); - currentInputConnection.setComposingText(word, word.length()); - - return true; - } - - - private boolean emoticonInPredictiveMode(int key) { - if (key != 1 || mInputMode != MODE_PREDICTIVE || !isNumKeyRepeated) { - return false; - } - - setSuggestions(Punctuation.Emoticons); setComposingTextFromCurrentSuggestion(); return true; } + private void handleSuggestions(ArrayList suggestions, int maxWordLength) { + setSuggestions(suggestions); - private boolean wordInPredictiveMode(int key) { - if ( - mInputMode != MODE_PREDICTIVE || - // 0 is not a word, but space, so we handle it in on0(). - key == 0 || - // double 1 is not a word, but an emoticon and it is handled elsewhere - (key == 1 && isNumKeyRepeated) - ) { - return false; - } - - - if ( - // Punctuation is considered "a word", so that we can increase the priority as needed - // Also, it must break the current word. - (key == 1 && predictionSequence.length() > 0) || - // On the other hand, letters also "break" punctuation. - (key != 1 && predictionSequence.endsWith("1")) - ) { - acceptCurrentSuggestion(); - } - - predictionSequence += key; - applyPredictionSequence(); - - return true; + // Put the first suggestion in the text field, + // but cut it off to the length of the sequence (how many keys were pressed), + // for a more intuitive experience. + String word = mSuggestionView.getCurrentSuggestion(); + word = word.substring(0, Math.min(maxWordLength, word.length())); + setComposingText(word); } - private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) { + private final Handler handleSuggestionsAsync = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { - ArrayList suggestions = msg.getData().getStringArrayList("suggestions"); - suggestions = guessSuggestionsWhenNone(suggestions, mSuggestionView.getCurrentSuggestion()); - - setSuggestions(suggestions); - mSuggestionView.changeCase(mTextCase, mLanguage.getLocale()); - - // Put the first suggestion in the text field, - // but cut it off to the length of the sequence (how many keys were pressed), - // for a more intuitive experience. - String word = mSuggestionView.getCurrentSuggestion(); - word = mSuggestionView.getCurrentSuggestion().substring(0, Math.min(predictionSequence.length(), word.length())); - currentInputConnection.setComposingText(word, word.length()); + handleSuggestions( + msg.getData().getStringArrayList("suggestions"), + msg.getData().getInt("maxWordLength", 1000) + ); } }; - private void applyPredictionSequence() { - if (predictionSequence.length() == 0) { - return; - } - - DictionaryDb.getSuggestions( - this, - handleSuggestions, - mLanguage, - predictionSequence, - prefs.getSuggestionsMin(), - prefs.getSuggestionsMax() - ); - } - - private ArrayList guessSuggestionsWhenNone(ArrayList suggestions, String lastWord) { - if ( - (suggestions != null && suggestions.size() > 0) || - predictionSequence.length() == 0 || - predictionSequence.charAt(0) == '1' - ) { - return suggestions; - } - - lastWord = lastWord.substring(0, Math.min(predictionSequence.length() - 1, lastWord.length())); - try { - int lastDigit = predictionSequence.charAt(predictionSequence.length() - 1) - '0'; - lastWord += mLanguage.getKeyCharacters(lastDigit).get(0); - } catch (Exception e) { - lastWord += predictionSequence.charAt(predictionSequence.length() - 1); - } - - return new ArrayList<>(Collections.singletonList(lastWord)); - } - - - private boolean nextSuggestionInModeAbc() { - return isNumKeyRepeated && mInputMode == MODE_ABC && nextSuggestion(); - } - private void acceptCurrentSuggestion() { - // bring this word up in the suggestions list next time - if (mInputMode == MODE_PREDICTIVE) { - String currentWord = mSuggestionView.getCurrentSuggestion(); - if (currentWord.length() == 0) { - Logger.i("acceptCurrentSuggestion", "Current word is empty. Nothing to accept."); - return; - } - - try { - String sequence = mLanguage.getDigitSequenceForWord(currentWord); - DictionaryDb.incrementWordFrequency(this, mLanguage, currentWord, sequence); - } catch (Exception e) { - Logger.e(getClass().getName(), "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage()); - } - } - + mInputMode.onAcceptSuggestion(mLanguage, mSuggestionView.getCurrentSuggestion()); commitCurrentSuggestion(); } private void commitCurrentSuggestion() { - predictionSequence = ""; - // commit the current suggestion to the input field if (!isSuggestionViewHidden()) { if (mSuggestionView.getCurrentSuggestion().equals(" ")) { // finishComposingText() seems to ignore a single space, // so we have to force commit it. - currentInputConnection.commitText(" ", 1); + setText(" "); } else { currentInputConnection.finishComposingText(); } @@ -435,34 +306,47 @@ public class TraditionalT9 extends KeyPadHandler { private void clearSuggestions() { + setSuggestions(null); + if (currentInputConnection != null) { - currentInputConnection.setComposingText("", 1); + setComposingTextFromCurrentSuggestion(); currentInputConnection.finishComposingText(); } - - setSuggestions(null); } - protected void setSuggestions(List suggestions) { - setSuggestions(suggestions, 0); + private void getSuggestions() { + if (!mInputMode.getSuggestionsAsync(handleSuggestionsAsync, mLanguage, mSuggestionView.getCurrentSuggestion())) { + handleSuggestions(mInputMode.getSuggestions(), 1); + } } - protected void setSuggestions(List suggestions, int initialSel) { + private void setSuggestions(List suggestions) { if (mSuggestionView == null) { return; } boolean show = suggestions != null && suggestions.size() > 0; - mSuggestionView.setSuggestions(suggestions, initialSel); + mSuggestionView.setSuggestions(suggestions, 0); + mSuggestionView.changeCase(mTextCase, mLanguage.getLocale()); setCandidatesViewShown(show); } + private void setText(String text) { + if (text != null) { + currentInputConnection.commitText(text, text.length()); + } + } - private void nextKeyMode() { + private void setComposingText(String text) { + currentInputConnection.setComposingText(text, 1); + } + + + private void nextInputMode() { if (mEditing == EDITING_STRICT_NUMERIC || mEditing == EDITING_DIALER) { clearSuggestions(); - mInputMode = MODE_123; + mInputMode = InputMode.getInstance(InputMode.MODE_123); } // when typing a word or viewing scrolling the suggestions, only change the case else if (!isSuggestionViewHidden()) { @@ -475,13 +359,13 @@ public class TraditionalT9 extends KeyPadHandler { setComposingTextFromCurrentSuggestion(); } // make "abc" and "ABC" separate modes from user perspective - else if (mInputMode == MODE_ABC && mTextCase == CASE_LOWER) { - mTextCase = CASE_UPPER; + else if (mInputMode.isABC() && mTextCase == InputMode.CASE_LOWER) { + mTextCase = InputMode.CASE_UPPER; } else { - int modeIndex = (allowedInputModes.indexOf(mInputMode) + 1) % allowedInputModes.size(); - mInputMode = allowedInputModes.get(modeIndex); + int modeIndex = (allowedInputModes.indexOf(mInputMode.getId()) + 1) % allowedInputModes.size(); + mInputMode = InputMode.getInstance(allowedInputModes.get(modeIndex)); - mTextCase = mInputMode == MODE_PREDICTIVE ? CASE_CAPITALIZE : CASE_LOWER; + mTextCase = mInputMode.isPredictive() ? InputMode.CASE_CAPITALIZE : InputMode.CASE_LOWER; } // save the settings for the next time @@ -493,7 +377,7 @@ public class TraditionalT9 extends KeyPadHandler { private void setComposingTextFromCurrentSuggestion() { if (!isSuggestionViewHidden()) { - currentInputConnection.setComposingText(mSuggestionView.getCurrentSuggestion(), 1); + setComposingText(mSuggestionView.getCurrentSuggestion()); } } @@ -522,18 +406,18 @@ public class TraditionalT9 extends KeyPadHandler { private void determineAllowedInputModes(EditorInfo inputField) { allowedInputModes = InputFieldHelper.determineInputModes(inputField); - int lastInputMode = prefs.getInputMode(); - if (allowedInputModes.contains(lastInputMode)) { - mInputMode = lastInputMode; - } else if (allowedInputModes.contains(TraditionalT9.MODE_ABC)) { - mInputMode = TraditionalT9.MODE_ABC; + int lastInputModeId = prefs.getInputMode(); + if (allowedInputModes.contains(lastInputModeId)) { + mInputMode = InputMode.getInstance(lastInputModeId); + } else if (allowedInputModes.contains(InputMode.MODE_ABC)) { + mInputMode = InputMode.getInstance(InputMode.MODE_ABC); } else { - mInputMode = allowedInputModes.get(0); + mInputMode = InputMode.getInstance(allowedInputModes.get(0)); } if (InputFieldHelper.isDialerField(inputField)) { mEditing = EDITING_DIALER; - } else if (mInputMode == TraditionalT9.MODE_123 && allowedInputModes.size() == 1) { + } else if (mInputMode.is123() && allowedInputModes.size() == 1) { mEditing = EDITING_STRICT_NUMERIC; } else { mEditing = InputFieldHelper.isFilterTextField(inputField) ? EDITING_NOSHOW : EDITING; @@ -542,20 +426,8 @@ public class TraditionalT9 extends KeyPadHandler { private void determineAllowedTextCases() { - // @todo: determine case from input - - allowedTextCases = new ArrayList<>(); - - if (mInputMode == TraditionalT9.MODE_PREDICTIVE) { - allowedTextCases.add(TraditionalT9.CASE_LOWER); - allowedTextCases.add(TraditionalT9.CASE_CAPITALIZE); - allowedTextCases.add(TraditionalT9.CASE_UPPER); - } else if (mInputMode == TraditionalT9.MODE_ABC) { - allowedTextCases.add(TraditionalT9.CASE_LOWER); - allowedTextCases.add(TraditionalT9.CASE_UPPER); - } else { - allowedTextCases.add(TraditionalT9.CASE_LOWER); - } + allowedTextCases = mInputMode.getAllowedTextCases(); + // @todo: determine the text case of the input and validate using the allowed ones } @@ -581,8 +453,8 @@ public class TraditionalT9 extends KeyPadHandler { try { Logger.d("restoreAddedWordIfAny", "Restoring word: '" + word + "'..."); - predictionSequence = mLanguage.getDigitSequenceForWord(word); - currentInputConnection.commitText(word, word.length()); + setText(word); + mInputMode.reset(); } catch (Exception e) { Logger.w("tt9/restoreLastWord", "Could not restore the last added word. " + e.getMessage()); } @@ -594,16 +466,6 @@ public class TraditionalT9 extends KeyPadHandler { * Generates the actual UI of TT9. */ protected View createSoftKeyView() { - if (softKeyView == null) { - softKeyView = getLayoutInflater().inflate(R.layout.mainview, null); - } - softKeyHandler.changeView(softKeyView); - return softKeyView; - } - - - private void displaySoftKeyMenu() { - createSoftKeyView(); - softKeyView.setVisibility(View.VISIBLE); + return softKeyHandler.createView(getLayoutInflater()); } } diff --git a/src/io/github/sspanak/tt9/ime/modes/InputMode.java b/src/io/github/sspanak/tt9/ime/modes/InputMode.java new file mode 100644 index 00000000..6cd007a9 --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/modes/InputMode.java @@ -0,0 +1,77 @@ +package io.github.sspanak.tt9.ime.modes; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import java.util.ArrayList; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.languages.Language; + +abstract public class InputMode { + // typing mode + public static final int MODE_PREDICTIVE = 0; + public static final int MODE_ABC = 1; + public static final int MODE_123 = 2; + + // text case + public static final int CASE_LOWER = 0; + public static final int CASE_CAPITALIZE = 1; + public static final int CASE_UPPER = 2; + protected ArrayList allowedTextCases = new ArrayList<>(); + + // data + protected ArrayList suggestions = new ArrayList<>(); + protected String word = null; + + + public static InputMode getInstance(int mode) { + switch(mode) { + case MODE_PREDICTIVE: + return new ModePredictive(); + case MODE_ABC: + return new ModeABC(); + default: + Logger.w("tt9/InputMode", "Defaulting to mode: " + Mode123.class.getName() + " for unknown InputMode: " + mode); + case MODE_123: + return new Mode123(); + } + } + + // Key handlers. Return "true" when handling the key or "false", when is nothing to do. + public boolean onBackspace() { return false; } + abstract public boolean onNumber(Language language, int key, boolean hold, boolean repeat); + + // Suggestions + public void onAcceptSuggestion(Language language, String suggestion) {} + public ArrayList getSuggestions() { return suggestions; } + public boolean getSuggestionsAsync(Handler handler, Language language, String lastWord) { return false; } + protected void sendSuggestions(Handler handler, ArrayList suggestions, int maxWordLength) { + Bundle bundle = new Bundle(); + bundle.putStringArrayList("suggestions", suggestions); + bundle.putInt("maxWordLength", maxWordLength); + Message msg = new Message(); + msg.setData(bundle); + handler.sendMessage(msg); + } + + // Word + public String getWord() { return word; } + + // Mode identifiers + public boolean isPredictive() { return false; } + public boolean isABC() { return false; } + public boolean is123() { return false; } + + // Utility + abstract public int getId(); + public ArrayList getAllowedTextCases() { return allowedTextCases; } + public void reset() { + suggestions = new ArrayList<>(); + word = null; + } + public boolean shouldTrackNumPress() { return true; } + public boolean shouldAcceptCurrentSuggestion(int key, boolean hold, boolean repeat) { return false; } + public boolean shouldSelectNextSuggestion() { return false; } +} diff --git a/src/io/github/sspanak/tt9/ime/modes/Mode123.java b/src/io/github/sspanak/tt9/ime/modes/Mode123.java new file mode 100644 index 00000000..6d264415 --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/modes/Mode123.java @@ -0,0 +1,28 @@ +package io.github.sspanak.tt9.ime.modes; + +import java.util.ArrayList; + +import io.github.sspanak.tt9.languages.Language; + +public class Mode123 extends InputMode { + public int getId() { return MODE_123; } + + Mode123() { + allowedTextCases.add(CASE_LOWER); + } + + + public boolean onNumber(Language l, int key, boolean hold, boolean repeat) { + if (key != 0) { + return false; + } + + suggestions = new ArrayList<>(); + word = hold ? "+" : "0"; + return true; + } + + + final public boolean is123() { return true; } + public boolean shouldTrackNumPress() { return false; } +} diff --git a/src/io/github/sspanak/tt9/ime/modes/ModeABC.java b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java new file mode 100644 index 00000000..512599b2 --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/modes/ModeABC.java @@ -0,0 +1,38 @@ +package io.github.sspanak.tt9.ime.modes; + +import io.github.sspanak.tt9.languages.Language; + +public class ModeABC extends InputMode { + public int getId() { return MODE_ABC; } + + private boolean shouldSelectNextLetter = false; + + ModeABC() { + allowedTextCases.add(CASE_LOWER); + allowedTextCases.add(CASE_UPPER); + } + + + public boolean onNumber(Language language, int key, boolean hold, boolean repeat) { + shouldSelectNextLetter = false; + suggestions = language.getKeyCharacters(key); + word = null; + + if (hold) { + suggestions = null; + word = String.valueOf(key); + } else if (repeat) { + suggestions = null; + shouldSelectNextLetter = true; + } + + return true; + } + + + final public boolean isABC() { return true; } + public boolean shouldAcceptCurrentSuggestion(int key, boolean hold, boolean repeat) { return hold || !repeat; } + public boolean shouldSelectNextSuggestion() { + return shouldSelectNextLetter; + } +} diff --git a/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java new file mode 100644 index 00000000..790bdc1b --- /dev/null +++ b/src/io/github/sspanak/tt9/ime/modes/ModePredictive.java @@ -0,0 +1,196 @@ +package io.github.sspanak.tt9.ime.modes; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Collections; + +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.db.DictionaryDb; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.Punctuation; +import io.github.sspanak.tt9.preferences.T9Preferences; + +public class ModePredictive extends InputMode { + public int getId() { return MODE_PREDICTIVE; } + + private Language currentLanguage = null; + private String digitSequence = ""; + private boolean isEmoticon = false; + private String lastInputFieldWord = ""; + private static Handler handleSuggestionsExternal; + + + + ModePredictive() { + allowedTextCases.add(CASE_CAPITALIZE); + allowedTextCases.add(CASE_LOWER); + allowedTextCases.add(CASE_UPPER); + } + + + public boolean onBackspace() { + if (digitSequence.length() < 1) { + return false; + } + + digitSequence = digitSequence.substring(0, digitSequence.length() - 1); + return true; + } + + + public boolean onNumber(Language l, int key, boolean hold, boolean repeat) { + isEmoticon = false; + + if (hold) { + // hold to type any digit + reset(); + word = String.valueOf(key); + } else if (key == 0) { + // "0" is " " + reset(); + word = " "; + } else if (key == 1 && repeat) { + // emoticons + reset(); + isEmoticon = true; + suggestions = Punctuation.Emoticons; + } + else { + // words + super.reset(); + digitSequence += key; + } + + return true; + } + + + public void reset() { + super.reset(); + digitSequence = ""; + } + + + final public boolean isPredictive() { + return true; + } + + + /** + * shouldAcceptCurrentSuggestion + * In this mode, In addition to confirming the suggestion in the input field, + * we also increase its' priority. This function determines whether we want to do all this or not. + */ + public boolean shouldAcceptCurrentSuggestion(int key, boolean hold, boolean repeat) { + return + hold + // Quickly accept suggestions using "space" instead of pressing "ok" then "space" + || key == 0 + // Punctuation is considered "a word", so that we can increase the priority as needed + // Also, it must break the current word. + || (key == 1 && digitSequence.length() > 0 && !digitSequence.endsWith("1")) + // On the other hand, letters also "break" punctuation. + || (key != 1 && digitSequence.endsWith("1")); + } + + + /** + * getSuggestionsAsync + * Queries the dictionary database for a list of suggestions matching the current language and + * sequence. Returns "false" when there is nothing to do. + * + * "lastWord" is used for generating suggestions when there are no results. + * See: generateSuggestionWhenNone() + */ + public boolean getSuggestionsAsync(Handler handler, Language language, String lastWord) { + if (isEmoticon) { + super.sendSuggestions(handler, suggestions, 2); + return true; + } + + if (digitSequence.length() == 0) { + return false; + } + + handleSuggestionsExternal = handler; + lastInputFieldWord = lastWord; + currentLanguage = language; + super.reset(); + + DictionaryDb.getSuggestions( + handleSuggestions, + language, + digitSequence, + T9Preferences.getInstance().getSuggestionsMin(), + T9Preferences.getInstance().getSuggestionsMax() + ); + + return true; + } + + + /** + * handleSuggestions + * Extracts the suggestions from the Message object and passes them to the actual external Handler. + * If there were no matches in the database, they will be generated based on the "lastInputFieldWord". + */ + private final Handler handleSuggestions = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + ArrayList suggestions = msg.getData().getStringArrayList("suggestions"); + suggestions = generateSuggestionWhenNone(suggestions, currentLanguage, lastInputFieldWord); + + ModePredictive.super.sendSuggestions(handleSuggestionsExternal, suggestions, digitSequence.length()); + } + }; + + /** + * generateSuggestionWhenNone + * When there are no matching suggestions after the last key press, generate a list of possible + * ones, so that the user can complete the missing word. + */ + private ArrayList generateSuggestionWhenNone(ArrayList suggestions, Language language, String lastWord) { + if ( + (lastWord == null || lastWord.length() == 0) || + (suggestions != null && suggestions.size() > 0) || + digitSequence.length() == 0 || + digitSequence.charAt(0) == '1' + ) { + return suggestions; + } + + lastWord = lastWord.substring(0, Math.min(digitSequence.length() - 1, lastWord.length())); + try { + int lastDigit = digitSequence.charAt(digitSequence.length() - 1) - '0'; + lastWord += language.getKeyCharacters(lastDigit).get(0); + } catch (Exception e) { + lastWord += digitSequence.charAt(digitSequence.length() - 1); + } + + return new ArrayList<>(Collections.singletonList(lastWord)); + } + + + /** + * onAcceptSuggestion + * Bring this word up in the suggestions list next time. + */ + public void onAcceptSuggestion(Language language, String currentWord) { + digitSequence = ""; + + if (currentWord.length() == 0) { + Logger.i("acceptCurrentSuggestion", "Current word is empty. Nothing to accept."); + return; + } + + try { + String sequence = language.getDigitSequenceForWord(currentWord); + DictionaryDb.incrementWordFrequency(language, currentWord, sequence); + } catch (Exception e) { + Logger.e("tt9/ModePredictive", "Failed incrementing priority of word: '" + currentWord + "'. " + e.getMessage()); + } + } +} diff --git a/src/io/github/sspanak/tt9/languages/Language.java b/src/io/github/sspanak/tt9/languages/Language.java index e5d5d4c9..5ee4d989 100644 --- a/src/io/github/sspanak/tt9/languages/Language.java +++ b/src/io/github/sspanak/tt9/languages/Language.java @@ -39,15 +39,11 @@ public class Language { } public ArrayList getKeyCharacters(int key) { - return getKeyCharacters(key, true); - } - - public ArrayList getKeyCharacters(int key, boolean lowerCase) { if (key < 0 || key >= characterMap.size()) { return new ArrayList<>(); } - ArrayList chars = lowerCase ? new ArrayList<>(characterMap.get(key)) : getUpperCaseChars(key); + ArrayList chars = new ArrayList<>(characterMap.get(key)); if (chars.size() > 0) { chars.add(String.valueOf(key)); } @@ -55,15 +51,6 @@ public class Language { return chars; } - private ArrayList getUpperCaseChars(int mapId) { - ArrayList uppercaseChars = new ArrayList<>(); - for (String ch : characterMap.get(mapId)) { - uppercaseChars.add(ch.toUpperCase(locale)); - } - - return uppercaseChars; - } - public String getDigitSequenceForWord(String word) throws Exception { StringBuilder sequence = new StringBuilder(); String lowerCaseWord = word.toLowerCase(locale); diff --git a/src/io/github/sspanak/tt9/preferences/T9Preferences.java b/src/io/github/sspanak/tt9/preferences/T9Preferences.java index b4f95538..eb06b7ea 100644 --- a/src/io/github/sspanak/tt9/preferences/T9Preferences.java +++ b/src/io/github/sspanak/tt9/preferences/T9Preferences.java @@ -11,6 +11,7 @@ import java.util.Arrays; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.LanguageCollection; @@ -23,13 +24,13 @@ public class T9Preferences { private final SharedPreferences.Editor prefsEditor; public T9Preferences (Context context) { - prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + prefs = PreferenceManager.getDefaultSharedPreferences(context); prefsEditor = prefs.edit(); } - public static T9Preferences getInstance(Context context) { + public static T9Preferences getInstance() { if (self == null) { - self = new T9Preferences(context); + self = new T9Preferences(TraditionalT9.getMainContext()); } return self; @@ -101,13 +102,13 @@ public class T9Preferences { } public int getTextCase() { - return prefs.getInt("pref_text_case", TraditionalT9.CASE_LOWER); + return prefs.getInt("pref_text_case", InputMode.CASE_LOWER); } public void saveTextCase(int textCase) { boolean isTextCaseValid = isIntInList( textCase, - new ArrayList<>(Arrays.asList(TraditionalT9.CASE_CAPITALIZE, TraditionalT9.CASE_LOWER, TraditionalT9.CASE_UPPER)), + new ArrayList<>(Arrays.asList(InputMode.CASE_CAPITALIZE, InputMode.CASE_LOWER, InputMode.CASE_UPPER)), "tt9/saveTextCase", "Not saving invalid text case: " + textCase ); @@ -131,21 +132,17 @@ public class T9Preferences { } public int getInputMode() { - return prefs.getInt("pref_input_mode", TraditionalT9.MODE_PREDICTIVE); + return prefs.getInt("pref_input_mode", InputMode.MODE_PREDICTIVE); } - public void saveInputMode(int mode) { - boolean isModeValid = isIntInList( - mode, - new ArrayList<>(Arrays.asList(TraditionalT9.MODE_123, TraditionalT9.MODE_ABC, TraditionalT9.MODE_PREDICTIVE)), - "tt9/saveInputMode", - "Not saving invalid text case: " + mode - ); - - if (isModeValid) { - prefsEditor.putInt("pref_input_mode", mode); - prefsEditor.apply(); + public void saveInputMode(InputMode mode) { + if (mode == null) { + Logger.w("tt9/saveInputMode", "Not saving NULL input mode"); + return; } + + prefsEditor.putInt("pref_input_mode", mode.getId()); + prefsEditor.apply(); } diff --git a/src/io/github/sspanak/tt9/settings_legacy/SettingMultiList.java b/src/io/github/sspanak/tt9/settings_legacy/SettingMultiList.java index 0ee9731a..796f5a64 100644 --- a/src/io/github/sspanak/tt9/settings_legacy/SettingMultiList.java +++ b/src/io/github/sspanak/tt9/settings_legacy/SettingMultiList.java @@ -17,7 +17,7 @@ public class SettingMultiList extends SettingList { public SettingMultiList (Context context, AttributeSet attrs, Object[] isettings) { super(context, attrs, isettings); selectedEntries = new boolean[entries.length]; - for (int langId : T9Preferences.getInstance(context).getEnabledLanguages()) { + for (int langId : T9Preferences.getInstance().getEnabledLanguages()) { selectedEntries[langId - 1] = true; // languages are 1-based, unlike arrays } summary = buildItems(); @@ -39,7 +39,7 @@ public class SettingMultiList extends SettingList { @Override public void onClick(DialogInterface dialog, int which) { if (id.equals("pref_lang_support")) { - T9Preferences.getInstance(context).saveEnabledLanguages(buildSelection()); + T9Preferences.getInstance().saveEnabledLanguages(buildSelection()); } summary = buildItems(); dialog.dismiss(); diff --git a/src/io/github/sspanak/tt9/ui/AddWordAct.java b/src/io/github/sspanak/tt9/ui/AddWordAct.java index cfcba285..decf3466 100644 --- a/src/io/github/sspanak/tt9/ui/AddWordAct.java +++ b/src/io/github/sspanak/tt9/ui/AddWordAct.java @@ -32,7 +32,7 @@ public class AddWordAct extends Activity { View v = getLayoutInflater().inflate(R.layout.addwordview, null); - EditText et = (EditText) v.findViewById(R.id.add_word_text); + EditText et = v.findViewById(R.id.add_word_text); et.setText(word); et.setSelection(word.length()); setContentView(v); @@ -46,7 +46,7 @@ public class AddWordAct extends Activity { switch (msg.what) { case 0: Logger.d("onAddedWord", "Added word: '" + word + "'..."); - T9Preferences.getInstance(main.getContext()).saveLastWord(word); + T9Preferences.getInstance().saveLastWord(word); break; case 1: @@ -71,7 +71,7 @@ public class AddWordAct extends Activity { word = ((EditText) main.findViewById(R.id.add_word_text)).getText().toString(); Logger.d("addWord", "Attempting to add word: '" + word + "'..."); - DictionaryDb.insertWord(this, onAddedWord, LanguageCollection.getLanguage(lang), word); + DictionaryDb.insertWord(onAddedWord, LanguageCollection.getLanguage(lang), word); } catch (InsertBlankWordException e) { Logger.e("AddWordAct.addWord", e.getMessage()); UI.toastLong(this, R.string.add_word_blank); diff --git a/src/io/github/sspanak/tt9/ui/CandidateView.java b/src/io/github/sspanak/tt9/ui/CandidateView.java index 2e1ea44e..f0dc34b5 100644 --- a/src/io/github/sspanak/tt9/ui/CandidateView.java +++ b/src/io/github/sspanak/tt9/ui/CandidateView.java @@ -13,7 +13,7 @@ import java.util.List; import java.util.Locale; import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.ime.modes.InputMode; public class CandidateView extends View { @@ -200,9 +200,9 @@ public class CandidateView extends View { ArrayList newSuggestions = new ArrayList<>(); for (String s : mSuggestions) { - if (textCase == TraditionalT9.CASE_LOWER) { + if (textCase == InputMode.CASE_LOWER) { newSuggestions.add(s.toLowerCase(locale)); - } else if (textCase == TraditionalT9.CASE_CAPITALIZE) { + } else if (textCase == InputMode.CASE_CAPITALIZE) { String cs = s.substring(0, 1).toUpperCase(locale) + s.substring(1).toLowerCase(locale); newSuggestions.add(cs); } else { diff --git a/src/io/github/sspanak/tt9/ui/TraditionalT9Settings.java b/src/io/github/sspanak/tt9/ui/TraditionalT9Settings.java index 0ac25b14..b9b43a0d 100644 --- a/src/io/github/sspanak/tt9/ui/TraditionalT9Settings.java +++ b/src/io/github/sspanak/tt9/ui/TraditionalT9Settings.java @@ -22,17 +22,6 @@ import android.widget.ListView; import com.stackoverflow.answer.UnicodeBOMInputStream; -import io.github.sspanak.tt9.Logger; -import io.github.sspanak.tt9.R; -import io.github.sspanak.tt9.db.DictionaryDb; -import io.github.sspanak.tt9.db.Word; -import io.github.sspanak.tt9.languages.Language; -import io.github.sspanak.tt9.languages.LanguageCollection; -import io.github.sspanak.tt9.preferences.T9Preferences; -import io.github.sspanak.tt9.settings_legacy.CustomInflater; -import io.github.sspanak.tt9.settings_legacy.Setting; -import io.github.sspanak.tt9.settings_legacy.SettingAdapter; - import java.io.BufferedReader; import java.io.Closeable; import java.io.File; @@ -47,6 +36,17 @@ import java.util.List; import java.util.Locale; import java.util.Properties; +import io.github.sspanak.tt9.Logger; +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.db.DictionaryDb; +import io.github.sspanak.tt9.db.Word; +import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.languages.LanguageCollection; +import io.github.sspanak.tt9.preferences.T9Preferences; +import io.github.sspanak.tt9.settings_legacy.CustomInflater; +import io.github.sspanak.tt9.settings_legacy.Setting; +import io.github.sspanak.tt9.settings_legacy.SettingAdapter; + public class TraditionalT9Settings extends ListActivity implements DialogInterface.OnCancelListener { AsyncTask task = null; @@ -205,7 +205,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa // add characters first, then dictionary: Logger.d("doInBackground", "Adding characters..."); - processChars(mContext, mSupportedLanguages); + processChars(mSupportedLanguages); Logger.d("doInBackground", "Characters added."); Logger.d("doInBackground", "Adding dict(s)..."); @@ -217,7 +217,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa if (internal) { try { dictstream = getAssets().open(dicts[x]); - reply = processFile(mContext, dictstream, reply, mSupportedLanguages.get(x), dicts[x]); + reply = processFile(dictstream, reply, mSupportedLanguages.get(x), dicts[x]); } catch (IOException e) { e.printStackTrace(); reply.status = false; @@ -227,7 +227,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa try { dictstream = new FileInputStream(new File( new File(Environment.getExternalStorageDirectory(), sddir), dicts[x])); - reply = processFile(mContext, dictstream, reply, mSupportedLanguages.get(x), dicts[x]); + reply = processFile(dictstream, reply, mSupportedLanguages.get(x), dicts[x]); } catch (FileNotFoundException e) { reply.status = false; reply.forceMsg("File not found: " + e.getMessage()); @@ -264,13 +264,13 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa * processChars * Inserts single characters. */ - private void processChars(Context context, List allLanguages) { + private void processChars(List allLanguages) { ArrayList list = new ArrayList<>(); try { for (Language lang : allLanguages) { for (int key = 0; key <= 9; key++) { - for (String langChar : lang.getKeyCharacters(key, true)) { + for (String langChar : lang.getKeyCharacters(key)) { if (langChar.length() == 1 && langChar.charAt(0) >= '0' && langChar.charAt(0) <= '9') { // We do not want 0-9 as "word suggestions" in Predictive mode. It looks confusing // when trying to type a word and also, one can type them by holding the respective @@ -289,7 +289,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa } } - DictionaryDb.insertWordsSync(context, list); + DictionaryDb.insertWordsSync(list); } catch (Exception e) { Logger.e("processChars", e.getMessage()); } @@ -306,7 +306,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa return null; } - private Reply processFile(Context context, InputStream is, Reply rpl, Language lang, String fname) + private Reply processFile(InputStream is, Reply rpl, Language lang, String fname) throws LoadException, IOException { long start = System.currentTimeMillis(); @@ -327,7 +327,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa try { - DictionaryDb.beginTransaction(context); + DictionaryDb.beginTransaction(); while (fileWord != null) { if (isCancelled()) { @@ -365,7 +365,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa dbWords.add(word); if (linecount % insertChunkSize == 0) { - DictionaryDb.insertWordsSync(context, dbWords); + DictionaryDb.insertWordsSync(dbWords); dbWords.clear(); } @@ -376,13 +376,13 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa fileWord = getLine(br, rpl, fname); } - DictionaryDb.insertWordsSync(context, dbWords); - DictionaryDb.endTransaction(context, true); + DictionaryDb.insertWordsSync(dbWords); + DictionaryDb.endTransaction(true); dbWords.clear(); publishProgress((int) ((float) pos / size * 10000)); } catch (Exception e) { - DictionaryDb.endTransaction(context, false); + DictionaryDb.endTransaction(false); Logger.e("processFile", e.getMessage()); } finally { br.close(); @@ -416,7 +416,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa // http://stackoverflow.com/questions/7645880/listview-with-onitemclicklistener-android // get settings - T9Preferences prefs = new T9Preferences(this); + T9Preferences prefs = new T9Preferences(getApplicationContext()); Object[] settings = { prefs.getInputMode() }; @@ -462,7 +462,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa UI.toast(mContext, R.string.dictionary_truncated); } }; - DictionaryDb.truncateWords(mContext, afterTruncate); + DictionaryDb.truncateWords(afterTruncate); } @@ -472,7 +472,7 @@ public class TraditionalT9Settings extends ListActivity implements DialogInterfa task = new LoadDictTask( msgid, internal, - LanguageCollection.getAll(T9Preferences.getInstance(mContext).getEnabledLanguages()) + LanguageCollection.getAll(T9Preferences.getInstance().getEnabledLanguages()) ); task.execute(); } diff --git a/src/io/github/sspanak/tt9/ui/UI.java b/src/io/github/sspanak/tt9/ui/UI.java index 5e70f6df..cc7ff2bc 100644 --- a/src/io/github/sspanak/tt9/ui/UI.java +++ b/src/io/github/sspanak/tt9/ui/UI.java @@ -7,6 +7,7 @@ import android.widget.Toast; import io.github.sspanak.tt9.Logger; import io.github.sspanak.tt9.R; import io.github.sspanak.tt9.ime.TraditionalT9; +import io.github.sspanak.tt9.ime.modes.InputMode; import io.github.sspanak.tt9.languages.Language; public class UI { @@ -34,21 +35,16 @@ public class UI { * Set the status icon that is appropriate in current mode (based on * openwmm-legacy) */ - public static void updateStatusIcon(TraditionalT9 tt9, Language inputLanguage, int inputMode, int textCase) { - switch (inputMode) { - case TraditionalT9.MODE_ABC: - tt9.showStatusIcon(inputLanguage.getAbcIcon(textCase == TraditionalT9.CASE_LOWER)); - break; - case TraditionalT9.MODE_PREDICTIVE: - tt9.showStatusIcon(inputLanguage.getIcon()); - break; - case TraditionalT9.MODE_123: - tt9.showStatusIcon(R.drawable.ime_number); - break; - default: - Logger.w("tt9.UI", "Unknown inputMode mode: " + inputMode + ". Hiding status icon."); - tt9.hideStatusIcon(); - break; + public static void updateStatusIcon(TraditionalT9 tt9, Language inputLanguage, InputMode inputMode, int textCase) { + if (inputMode.isABC()) { + tt9.showStatusIcon(inputLanguage.getAbcIcon(textCase == InputMode.CASE_LOWER)); + } else if (inputMode.isPredictive()) { + tt9.showStatusIcon(inputLanguage.getIcon()); + } else if (inputMode.is123()) { + tt9.showStatusIcon(R.drawable.ime_number); + } else { + Logger.w("tt9.UI", "Unknown inputMode mode: " + inputMode + ". Hiding status icon."); + tt9.hideStatusIcon(); } }