diff options
Diffstat (limited to 'java/src')
8 files changed, 536 insertions, 114 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index 2b1cc43cd..07b9c1e8c 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -31,6 +31,7 @@ import com.android.inputmethod.keyboard.internal.KeyStyles; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.Utils; import com.android.inputmethod.latin.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; @@ -715,22 +716,30 @@ public class Keyboard { R.styleable.Keyboard_Key); try { final int displayHeight = mDisplayMetrics.heightPixels; - final int keyboardHeight = (int)keyboardAttr.getDimension( - R.styleable.Keyboard_keyboardHeight, displayHeight / 2); - final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, + final String keyboardHeightString = Utils.getDeviceOverrideValue( + mResources, R.array.keyboard_heights, null); + final float keyboardHeight; + if (keyboardHeightString != null) { + keyboardHeight = Float.parseFloat(keyboardHeightString) + * mDisplayMetrics.density; + } else { + keyboardHeight = keyboardAttr.getDimension( + R.styleable.Keyboard_keyboardHeight, displayHeight / 2); + } + final float maxKeyboardHeight = getDimensionOrFraction(keyboardAttr, R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2); - int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, + float minKeyboardHeight = getDimensionOrFraction(keyboardAttr, R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2); if (minKeyboardHeight < 0) { // Specified fraction was negative, so it should be calculated against display // width. - minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr, + minKeyboardHeight = -getDimensionOrFraction(keyboardAttr, R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2); } final Params params = mParams; // Keyboard height will not exceed maxKeyboardHeight and will not be less than // minKeyboardHeight. - params.mOccupiedHeight = Math.max( + params.mOccupiedHeight = (int)Math.max( Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); params.mOccupiedWidth = params.mId.mWidth; params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr, diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java index b66d1661d..3f6c37477 100644 --- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java @@ -46,6 +46,7 @@ import com.android.inputmethod.keyboard.internal.KeySpecParser; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.ResearchLogger; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.StringUtils; import com.android.inputmethod.latin.SubtypeUtils; @@ -66,6 +67,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke SuddenJumpingTouchEventHandler.ProcessMotionEvent { private static final String TAG = LatinKeyboardView.class.getSimpleName(); + // TODO: Kill process when the usability study mode was changed. + private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy; + /** Listener for {@link KeyboardActionListener}. */ private KeyboardActionListener mKeyboardActionListener; @@ -653,8 +657,6 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke final int index = me.getActionIndex(); final int id = me.getPointerId(index); final int x, y; - final float size = me.getSize(index); - final float pressure = me.getPressure(index); if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) { x = mMoreKeysPanel.translateX((int)me.getX(index)); y = mMoreKeysPanel.translateY((int)me.getY(index)); @@ -662,10 +664,44 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke x = (int)me.getX(index); y = (int)me.getY(index); } - if (LatinImeLogger.sUsabilityStudy) { + if (ENABLE_USABILITY_STUDY_LOG) { + final String eventTag; + switch (action) { + case MotionEvent.ACTION_UP: + eventTag = "[Up]"; + break; + case MotionEvent.ACTION_DOWN: + eventTag = "[Down]"; + break; + case MotionEvent.ACTION_POINTER_UP: + eventTag = "[PointerUp]"; + break; + case MotionEvent.ACTION_POINTER_DOWN: + eventTag = "[PointerDown]"; + break; + case MotionEvent.ACTION_MOVE: // Skip this as being logged below + eventTag = ""; + break; + default: + eventTag = "[Action" + action + "]"; + break; + } + if (!TextUtils.isEmpty(eventTag)) { + final float size = me.getSize(index); + final float pressure = me.getPressure(index); + UsabilityStudyLogUtils.getInstance().write( + eventTag + eventTime + "," + id + "," + x + "," + y + "," + + size + "," + pressure); + } + } + if (ResearchLogger.sIsLogging) { + // TODO: remove redundant calculations of size and pressure by + // removing UsabilityStudyLog code once the ResearchLogger is mature enough + final float size = me.getSize(index); + final float pressure = me.getPressure(index); if (action != MotionEvent.ACTION_MOVE) { // Skip ACTION_MOVE events as they are logged below - UsabilityStudyLogUtils.getInstance().writeMotionEvent(action, eventTime, id, x, + ResearchLogger.getInstance().logMotionEvent(action, eventTime, id, x, y, size, pressure); } } @@ -714,8 +750,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke if (action == MotionEvent.ACTION_MOVE) { for (int i = 0; i < pointerCount; i++) { + final int pointerId = me.getPointerId(i); final PointerTracker tracker = PointerTracker.getPointerTracker( - me.getPointerId(i), this); + pointerId, this); final int px, py; if (mMoreKeysPanel != null && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) { @@ -726,9 +763,19 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke py = (int)me.getY(i); } tracker.onMoveEvent(px, py, eventTime); - if (LatinImeLogger.sUsabilityStudy) { - UsabilityStudyLogUtils.getInstance().writeMotionEvent(action, eventTime, id, - px, py, size, pressure); + if (ENABLE_USABILITY_STUDY_LOG) { + final float pointerSize = me.getSize(i); + final float pointerPressure = me.getPressure(i); + UsabilityStudyLogUtils.getInstance().write("[Move]" + eventTime + "," + + pointerId + "," + px + "," + py + "," + + pointerSize + "," + pointerPressure); + } + if (ResearchLogger.sIsLogging) { + // TODO: earlier comment about redundant calculations applies here too + final float pointerSize = me.getSize(i); + final float pointerPressure = me.getPressure(i); + ResearchLogger.getInstance().logMotionEvent(action, eventTime, pointerId, + px, py, pointerSize, pointerPressure); } } } else { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index e07bba065..7272006a2 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -439,6 +439,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs = prefs; LatinImeLogger.init(this, prefs); + ResearchLogger.init(this, prefs); LanguageSwitcherProxy.init(this, prefs); InputMethodManagerCompatWrapper.init(this); SubtypeSwitcher.init(this); @@ -1006,10 +1007,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final int touchHeight = inputView.getHeight() + extraHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; - if (DEBUG) { - Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth - + " height=" + touchHeight); - } setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight); } outInsets.contentTopInsets = touchY; @@ -1267,8 +1264,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } mLastKeyTime = when; - if (LatinImeLogger.sUsabilityStudy) { - UsabilityStudyLogUtils.getInstance().writeKeyEvent(primaryCode, x, y); + if (ResearchLogger.sIsLogging) { + ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y); } final KeyboardSwitcher switcher = mKeyboardSwitcher; diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java new file mode 100644 index 000000000..6ba9118d6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import android.content.SharedPreferences; +import android.inputmethodservice.InputMethodService; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.inputmethod.keyboard.Keyboard; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Logs the use of the LatinIME keyboard. + * + * This class logs operations on the IME keyboard, including what the user has typed. + * Data is stored locally in a file in app-specific storage. + * + * This functionality is off by default. + */ +public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = ResearchLogger.class.getSimpleName(); + private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; + + private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager()); + public static boolean sIsLogging = false; + private final Handler mLoggingHandler; + private InputMethodService mIms; + private final Date mDate; + private final SimpleDateFormat mDateFormat; + + /** + * Isolates management of files. This variable should never be null, but can be changed + * to support testing. + */ + private LogFileManager mLogFileManager; + + /** + * Manages the file(s) that stores the logs. + * + * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access + * the logs. + */ + public static class LogFileManager { + private static final String DEFAULT_FILENAME = "log.txt"; + private static final String DEFAULT_LOG_DIRECTORY = "researchLogger"; + + private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24; + + private InputMethodService mIms; + private File mFile; + private PrintWriter mPrintWriter; + + /* package */ LogFileManager() { + } + + public void init(InputMethodService ims) { + mIms = ims; + } + + public synchronized void createLogFile() { + try { + createLogFile(DEFAULT_LOG_DIRECTORY, DEFAULT_FILENAME); + } catch (FileNotFoundException e) { + Log.w(TAG, e); + } + } + + public synchronized void createLogFile(String dir, String filename) + throws FileNotFoundException { + if (mIms == null) { + Log.w(TAG, "InputMethodService is not configured. Logging is off."); + return; + } + File filesDir = mIms.getFilesDir(); + if (filesDir == null || !filesDir.exists()) { + Log.w(TAG, "Storage directory does not exist. Logging is off."); + return; + } + File directory = new File(filesDir, dir); + if (!directory.exists()) { + boolean wasCreated = directory.mkdirs(); + if (!wasCreated) { + Log.w(TAG, "Log directory cannot be created. Logging is off."); + return; + } + } + + close(); + mFile = new File(directory, filename); + boolean append = true; + if (mFile.exists() && mFile.lastModified() + LOGFILE_PURGE_INTERVAL < + System.currentTimeMillis()) { + append = false; + } + mPrintWriter = new PrintWriter(new FileOutputStream(mFile, append), true); + } + + public synchronized boolean append(String s) { + if (mPrintWriter == null) { + Log.w(TAG, "PrintWriter is null"); + return false; + } else { + mPrintWriter.print(s); + return !mPrintWriter.checkError(); + } + } + + public synchronized void reset() { + if (mPrintWriter != null) { + mPrintWriter.close(); + mPrintWriter = null; + } + if (mFile != null && mFile.exists()) { + mFile.delete(); + mFile = null; + } + } + + public synchronized void close() { + if (mPrintWriter != null) { + mPrintWriter.close(); + mPrintWriter = null; + mFile = null; + } + } + } + + private ResearchLogger(LogFileManager logFileManager) { + mDate = new Date(); + mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ"); + + HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task", + Process.THREAD_PRIORITY_BACKGROUND); + handlerThread.start(); + mLoggingHandler = new Handler(handlerThread.getLooper()); + mLogFileManager = logFileManager; + } + + public static ResearchLogger getInstance() { + return sInstance; + } + + public static void init(InputMethodService ims, SharedPreferences prefs) { + sInstance.initInternal(ims, prefs); + } + + public void initInternal(InputMethodService ims, SharedPreferences prefs) { + mIms = ims; + if (mLogFileManager != null) { + mLogFileManager.init(ims); + mLogFileManager.createLogFile(); + } + if (prefs != null) { + sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); + } + prefs.registerOnSharedPreferenceChangeListener(this); + } + + /** + * Change to a different logFileManager. Will not allow it to be set to null. + */ + /* package */ void setLogFileManager(ResearchLogger.LogFileManager manager) { + if (manager == null) { + Log.w(TAG, "warning: trying to set null logFileManager. ignoring."); + } else { + mLogFileManager = manager; + } + } + + /** + * Represents a category of logging events that share the same subfield structure. + */ + private static enum LogGroup { + MOTION_EVENT("m"), + KEY("k"), + CORRECTION("c"), + STATE_CHANGE("s"); + + private final String mLogString; + + private LogGroup(String logString) { + mLogString = logString; + } + } + + public void logMotionEvent(final int action, final long eventTime, final int id, + final int x, final int y, final float size, final float pressure) { + final String eventTag; + switch (action) { + case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break; + case MotionEvent.ACTION_UP: eventTag = "[Up]"; break; + case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break; + case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break; + case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break; + case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break; + case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break; + default: eventTag = "[Action" + action + "]"; break; + } + if (!TextUtils.isEmpty(eventTag)) { + StringBuilder sb = new StringBuilder(); + sb.append(eventTag); + sb.append('\t'); sb.append(eventTime); + sb.append('\t'); sb.append(id); + sb.append('\t'); sb.append(x); + sb.append('\t'); sb.append(y); + sb.append('\t'); sb.append(size); + sb.append('\t'); sb.append(pressure); + write(LogGroup.MOTION_EVENT, sb.toString()); + } + } + + public void logKeyEvent(int code, int x, int y) { + final StringBuilder sb = new StringBuilder(); + sb.append(Keyboard.printableCode(code)); + sb.append('\t'); sb.append(x); + sb.append('\t'); sb.append(y); + write(LogGroup.KEY, sb.toString()); + + LatinImeLogger.onPrintAllUsabilityStudyLogs(); + } + + public void logCorrection(String subgroup, String before, String after, int position) { + final StringBuilder sb = new StringBuilder(); + sb.append(subgroup); + sb.append('\t'); sb.append(before); + sb.append('\t'); sb.append(after); + sb.append('\t'); sb.append(position); + write(LogGroup.CORRECTION, sb.toString()); + } + + public void logStateChange(String subgroup, String details) { + write(LogGroup.STATE_CHANGE, subgroup + "\t" + details); + } + + private void write(final LogGroup logGroup, final String log) { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + final long currentTime = System.currentTimeMillis(); + mDate.setTime(currentTime); + final long upTime = SystemClock.uptimeMillis(); + + final String printString = String.format("%s\t%d\t%s\t%s\n", + mDateFormat.format(mDate), upTime, logGroup.mLogString, log); + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log); + } + if (mLogFileManager.append(printString)) { + // success + } else { + if (LatinImeLogger.sDBG) { + Log.w(TAG, "Unable to write to log."); + } + } + } + }); + } + + public void clearAll() { + mLoggingHandler.post(new Runnable() { + @Override + public void run() { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Delete log file."); + } + mLogFileManager.reset(); + } + }); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if (key == null || prefs == null) { + return; + } + sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); + } +} diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index a3589da0a..be64c2fd8 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -220,6 +220,7 @@ public class Utils { } public static class UsabilityStudyLogUtils { + // TODO: remove code duplication with ResearchLog class private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); private static final String FILENAME = "log.txt"; private static final UsabilityStudyLogUtils sInstance = @@ -262,73 +263,28 @@ public class Utils { } } - /** - * Represents a category of logging events that share the same subfield structure. - */ - public static enum LogGroup { - MOTION_EVENT("m"), - KEY("k"), - CORRECTION("c"), - STATE_CHANGE("s"); - - private final String mLogString; - - private LogGroup(String logString) { - mLogString = logString; - } + public static void writeBackSpace(int x, int y) { + UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y); } - public void writeMotionEvent(final int action, final long eventTime, final int id, - final int x, final int y, final float size, final float pressure) { - final String eventTag; - switch (action) { - case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break; - case MotionEvent.ACTION_UP: eventTag = "[Up]"; break; - case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break; - case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break; - case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break; - case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break; - case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break; - default: eventTag = "[Action" + action + "]"; break; - } - if (!TextUtils.isEmpty(eventTag)) { - StringBuilder sb = new StringBuilder(); - sb.append(eventTag); - sb.append('\t'); sb.append(eventTime); - sb.append('\t'); sb.append(id); - sb.append('\t'); sb.append(x); - sb.append('\t'); sb.append(y); - sb.append('\t'); sb.append(size); - sb.append('\t'); sb.append(pressure); - write(LogGroup.MOTION_EVENT, sb.toString()); + public void writeChar(char c, int x, int y) { + String inputChar = String.valueOf(c); + switch (c) { + case '\n': + inputChar = "<enter>"; + break; + case '\t': + inputChar = "<tab>"; + break; + case ' ': + inputChar = "<space>"; + break; } - } - - public void writeKeyEvent(int code, int x, int y) { - final StringBuilder sb = new StringBuilder(); - sb.append(Keyboard.printableCode(code)); - sb.append('\t'); sb.append(x); - sb.append('\t'); sb.append(y); - write(LogGroup.KEY, sb.toString()); - - // TODO: replace with a cleaner flush+retrieve mechanism + UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y); LatinImeLogger.onPrintAllUsabilityStudyLogs(); } - public void writeCorrection(String subgroup, String before, String after, int position) { - final StringBuilder sb = new StringBuilder(); - sb.append(subgroup); - sb.append('\t'); sb.append(before); - sb.append('\t'); sb.append(after); - sb.append('\t'); sb.append(position); - write(LogGroup.CORRECTION, sb.toString()); - } - - public void writeStateChange(String subgroup, String details) { - write(LogGroup.STATE_CHANGE, subgroup + "\t" + details); - } - - private void write(final LogGroup logGroup, final String log) { + public void write(final String log) { mLoggingHandler.post(new Runnable() { @Override public void run() { @@ -336,8 +292,8 @@ public class Utils { final long currentTime = System.currentTimeMillis(); mDate.setTime(currentTime); - final String printString = String.format("%s\t%d\t%s\t%s\n", - mDateFormat.format(mDate), currentTime, logGroup.mLogString, log); + final String printString = String.format("%s\t%d\t%s\n", + mDateFormat.format(mDate), currentTime, log); if (LatinImeLogger.sDBG) { Log.d(USABILITY_TAG, "Write: " + log); } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 70530c338..64fcd7f1a 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -59,7 +59,7 @@ public class FusionDictionary implements Iterable<Word> { */ public static class WeightedString { final String mWord; - final int mFrequency; + int mFrequency; public WeightedString(String word, int frequency) { mWord = word; mFrequency = frequency; @@ -94,10 +94,10 @@ public class FusionDictionary implements Iterable<Word> { public static class CharGroup { public static final int NOT_A_TERMINAL = -1; final int mChars[]; - final ArrayList<WeightedString> mShortcutTargets; - final ArrayList<WeightedString> mBigrams; - final int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. - final boolean mIsShortcutOnly; // Only valid if this is a terminal. + ArrayList<WeightedString> mShortcutTargets; + ArrayList<WeightedString> mBigrams; + int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. + boolean mIsShortcutOnly; // Only valid if this is a terminal. Node mChildren; // The two following members to help with binary generation int mCachedSize; @@ -146,6 +146,102 @@ public class FusionDictionary implements Iterable<Word> { assert(mChars.length > 0); return 1 < mChars.length; } + + /** + * Adds a word to the bigram list. Updates the frequency if the word already + * exists. + */ + public void addBigram(final String word, final int frequency) { + if (mBigrams == null) { + mBigrams = new ArrayList<WeightedString>(); + } + WeightedString bigram = getBigram(word); + if (bigram != null) { + bigram.mFrequency = frequency; + } else { + bigram = new WeightedString(word, frequency); + mBigrams.add(bigram); + } + } + + /** + * Gets the shortcut target for the given word. Returns null if the word is not in the + * shortcut list. + */ + public WeightedString getShortcut(final String word) { + if (mShortcutTargets != null) { + final int size = mShortcutTargets.size(); + for (int i = 0; i < size; ++i) { + WeightedString shortcut = mShortcutTargets.get(i); + if (shortcut.mWord.equals(word)) { + return shortcut; + } + } + } + return null; + } + + /** + * Gets the bigram for the given word. + * Returns null if the word is not in the bigrams list. + */ + public WeightedString getBigram(final String word) { + if (mBigrams != null) { + final int size = mBigrams.size(); + for (int i = 0; i < size; ++i) { + WeightedString bigram = mBigrams.get(i); + if (bigram.mWord.equals(word)) { + return bigram; + } + } + } + return null; + } + + /** + * Updates the CharGroup with the given properties. Adds the shortcut and bigram lists to + * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only + * updated if they are higher than the existing ones. + */ + public void update(int frequency, ArrayList<WeightedString> shortcutTargets, + ArrayList<WeightedString> bigrams, boolean isShortcutOnly) { + if (frequency > mFrequency) { + mFrequency = frequency; + } + if (shortcutTargets != null) { + if (mShortcutTargets == null) { + mShortcutTargets = shortcutTargets; + } else { + final int size = shortcutTargets.size(); + for (int i = 0; i < size; ++i) { + final WeightedString shortcut = shortcutTargets.get(i); + final WeightedString existingShortcut = getShortcut(shortcut.mWord); + if (existingShortcut == null) { + mShortcutTargets.add(shortcut); + } else if (existingShortcut.mFrequency < shortcut.mFrequency) { + existingShortcut.mFrequency = shortcut.mFrequency; + } + } + } + } + if (bigrams != null) { + if (mBigrams == null) { + mBigrams = bigrams; + } else { + final int size = bigrams.size(); + for (int i = 0; i < size; ++i) { + final WeightedString bigram = bigrams.get(i); + final WeightedString existingBigram = getBigram(bigram.mWord); + if (existingBigram == null) { + mBigrams.add(bigram); + } else if (existingBigram.mFrequency < bigram.mFrequency) { + existingBigram.mFrequency = bigram.mFrequency; + } + } + } + } + mIsShortcutOnly = isShortcutOnly; + } } /** @@ -259,6 +355,27 @@ public class FusionDictionary implements Iterable<Word> { } /** + * Helper method to add a new bigram to the dictionary. + * + * @param word1 the previous word of the context + * @param word2 the next word of the context + * @param frequency the bigram frequency + */ + public void setBigram(final String word1, final String word2, final int frequency) { + CharGroup charGroup = findWordInTree(mRoot, word1); + if (charGroup != null) { + final CharGroup charGroup2 = findWordInTree(mRoot, word2); + if (charGroup2 == null) { + // TODO: refactor with the identical code in addNeutralWords + add(getCodePoints(word2), 0, null, null, false /* isShortcutOnly */); + } + charGroup.addBigram(word2, frequency); + } else { + throw new RuntimeException("First word of bigram not found"); + } + } + + /** * Add a word to this dictionary. * * The shortcuts and bigrams, if any, have to be in the dictionary already. If they aren't, @@ -306,17 +423,9 @@ public class FusionDictionary implements Iterable<Word> { if (differentCharIndex == currentGroup.mChars.length) { if (charIndex + differentCharIndex >= word.length) { // The new word is a prefix of an existing word, but the node on which it - // should end already exists as is. - if (currentGroup.mFrequency > 0) { - throw new RuntimeException("Such a word already exists in the dictionary : " - + new String(word, 0, word.length)); - } else { - final CharGroup newNode = new CharGroup(currentGroup.mChars, - shortcutTargets, bigrams, frequency, currentGroup.mChildren, - isShortcutOnly); - currentNode.mData.set(nodeIndex, newNode); - checkStack(currentNode); - } + // should end already exists as is. Since the old CharNode was not a terminal, + // make it one by filling in its frequency and other attributes + currentGroup.update(frequency, shortcutTargets, bigrams, isShortcutOnly); } else { // The new word matches the full old word and extends past it. // We only have to create a new node and add it to the end of this. @@ -328,19 +437,9 @@ public class FusionDictionary implements Iterable<Word> { } } else { if (0 == differentCharIndex) { - // Exact same word. Check the frequency is 0 or NOT_A_TERMINAL, and update. - if (0 != frequency) { - if (0 < currentGroup.mFrequency) { - throw new RuntimeException("This word already exists with frequency " - + currentGroup.mFrequency + " : " - + new String(word, 0, word.length)); - } - final CharGroup newGroup = new CharGroup(word, - currentGroup.mShortcutTargets, currentGroup.mBigrams, - frequency, currentGroup.mChildren, - currentGroup.mIsShortcutOnly && isShortcutOnly); - currentNode.mData.set(nodeIndex, newGroup); - } + // Exact same word. Update the frequency if higher. This will also add the + // new bigrams to the existing bigram list if it already exists. + currentGroup.update(frequency, shortcutTargets, bigrams, isShortcutOnly); } else { // Partial prefix match only. We have to replace the current node with a node // containing the current prefix and create two new ones for the tails. diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 973a448ee..cd34ba832 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -574,7 +574,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService // The getXYForCodePointAndScript method returns (Y << 16) + X final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript( codePoint, mScript); - composer.add(codePoint, xy & 0xFFFF, xy >> 16, null); + if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) { + composer.add(codePoint, WordComposer.NOT_A_COORDINATE, + WordComposer.NOT_A_COORDINATE, null); + } else { + composer.add(codePoint, xy & 0xFFFF, xy >> 16, null); + } } final int capitalizeType = getCapitalizationType(text); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index 7627700dd..0103e8423 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -35,6 +35,9 @@ public class SpellCheckerProximityInfo { // The number of rows in the grid used by the spell checker. final public static int PROXIMITY_GRID_HEIGHT = 3; + final private static int NOT_AN_INDEX = -1; + final public static int NOT_A_COORDINATE_PAIR = -1; + // Helper methods final protected static void buildProximityIndices(final int[] proximity, final TreeMap<Integer, Integer> indices) { @@ -45,7 +48,7 @@ public class SpellCheckerProximityInfo { final protected static int computeIndex(final int characterCode, final TreeMap<Integer, Integer> indices) { final Integer result = indices.get(characterCode); - if (null == result) return -1; + if (null == result) return NOT_AN_INDEX; return result; } @@ -196,8 +199,10 @@ public class SpellCheckerProximityInfo { // Returns (Y << 16) + X to avoid creating a temporary object. This is okay because // X and Y are limited to PROXIMITY_GRID_WIDTH resp. PROXIMITY_GRID_HEIGHT which is very // inferior to 1 << 16 + // As an exception, this returns NOT_A_COORDINATE_PAIR if the key is not on the grid public static int getXYForCodePointAndScript(final int codePoint, final int script) { final int index = getIndexOfCodeForScript(codePoint, script); + if (NOT_AN_INDEX == index) return NOT_A_COORDINATE_PAIR; final int y = index / PROXIMITY_GRID_WIDTH; final int x = index % PROXIMITY_GRID_WIDTH; if (y > PROXIMITY_GRID_HEIGHT) { |