aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java37
-rw-r--r--java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java10
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java11
-rw-r--r--java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java16
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java3
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java4
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java2
-rw-r--r--java/src/com/android/inputmethod/latin/LatinImeLogger.java2
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java23
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java52
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java1208
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java48
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java677
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/MakedictLog.java40
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java32
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java (renamed from java/src/com/android/inputmethod/latin/makedict/Dummy.java)11
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Word.java72
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java8
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java8
19 files changed, 2183 insertions, 81 deletions
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 847174c0a..24eb7592b 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -99,7 +99,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Main keyboard
private Keyboard mKeyboard;
- private final KeyDrawParams mKeyDrawParams;
+ protected final KeyDrawParams mKeyDrawParams;
// Key preview
private final int mKeyPreviewLayoutId;
@@ -171,7 +171,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
}
- /* package */ static class KeyDrawParams {
+ protected static class KeyDrawParams {
// XML attributes
public final int mKeyTextColor;
public final int mKeyTextInactivatedColor;
@@ -351,7 +351,6 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
mPaint.setAntiAlias(true);
- mPaint.setTextAlign(Align.CENTER);
}
// Read fraction value in TypedArray as float.
@@ -729,10 +728,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
}
- private static int getCharGeometryCacheKey(char reference, Paint paint) {
+ private static int getCharGeometryCacheKey(char referenceChar, Paint paint) {
final int labelSize = (int)paint.getTextSize();
final Typeface face = paint.getTypeface();
- final int codePointOffset = reference << 15;
+ final int codePointOffset = referenceChar << 15;
if (face == Typeface.DEFAULT) {
return codePointOffset + labelSize;
} else if (face == Typeface.DEFAULT_BOLD) {
@@ -747,41 +746,35 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Working variable for the following methods.
private final Rect mTextBounds = new Rect();
- private float getCharHeight(char[] character, Paint paint) {
- final Integer key = getCharGeometryCacheKey(character[0], paint);
+ private float getCharHeight(char[] referenceChar, Paint paint) {
+ final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextHeightCache.get(key);
if (cachedValue != null)
return cachedValue;
- paint.getTextBounds(character, 0, 1, mTextBounds);
+ paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
final float height = mTextBounds.height();
sTextHeightCache.put(key, height);
return height;
}
- private float getCharWidth(char[] character, Paint paint) {
- final Integer key = getCharGeometryCacheKey(character[0], paint);
+ private float getCharWidth(char[] referenceChar, Paint paint) {
+ final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextWidthCache.get(key);
if (cachedValue != null)
return cachedValue;
- paint.getTextBounds(character, 0, 1, mTextBounds);
+ paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
final float width = mTextBounds.width();
sTextWidthCache.put(key, width);
return width;
}
- protected float getLabelWidth(CharSequence label, Paint paint) {
+ public float getLabelWidth(String label, Paint paint) {
paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds);
return mTextBounds.width();
}
- public float getDefaultLabelWidth(String label, Paint paint) {
- paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
- paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
- return getLabelWidth(label, paint);
- }
-
protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
int height) {
canvas.translate(x, y);
@@ -815,6 +808,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
canvas.translate(-x, -y);
}
+ public Paint newDefaultLabelPaint() {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
+ paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
+ return paint;
+ }
+
public void cancelAllMessages() {
mDrawingHandler.cancelAllMessages();
}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
index b869059e4..da7d01af4 100644
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
@@ -54,6 +54,7 @@ import com.android.inputmethod.latin.StringUtils;
import com.android.inputmethod.latin.SubtypeUtils;
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
+import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils.LogGroup;
import java.util.Locale;
import java.util.WeakHashMap;
@@ -701,7 +702,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
break;
}
if (!TextUtils.isEmpty(eventTag)) {
- UsabilityStudyLogUtils.getInstance().write(
+ UsabilityStudyLogUtils.getInstance().write(LogGroup.MOTION_EVENT,
eventTag + eventTime + "," + id + "," + x + "," + y + ","
+ me.getSize(index) + "," + me.getPressure(index));
}
@@ -764,9 +765,10 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
tracker.onMoveEvent(px, py, eventTime);
if (ENABLE_USABILITY_STUDY_LOG) {
- UsabilityStudyLogUtils.getInstance().write("[Move]" + eventTime + ","
- + me.getPointerId(i) + "," + px + "," + py + ","
- + me.getSize(i) + "," + me.getPressure(i));
+ UsabilityStudyLogUtils.getInstance().write(
+ LogGroup.MOTION_EVENT,
+ "[Move]" + eventTime + "," + me.getPointerId(i) + "," + px + "," + py
+ + "," + me.getSize(i) + "," + me.getPressure(i));
}
}
} else {
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index 904a81de4..72a5d0f05 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -296,17 +296,16 @@ public class MoreKeysKeyboard extends Keyboard {
final int padding = (int)(view.getResources()
.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
+ (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0));
- Paint paint = null;
+ final Paint paint = view.newDefaultLabelPaint();
+ paint.setTextSize(parentKey.hasLabelsInMoreKeys()
+ ? view.mKeyDrawParams.mKeyLabelSize
+ : view.mKeyDrawParams.mKeyLetterSize);
int maxWidth = minKeyWidth;
for (String moreKeySpec : parentKey.mMoreKeys) {
final String label = KeySpecParser.getLabel(moreKeySpec);
// If the label is single letter, minKeyWidth is enough to hold the label.
if (label != null && StringUtils.codePointCount(label) > 1) {
- if (paint == null) {
- paint = new Paint();
- paint.setAntiAlias(true);
- }
- final int width = (int)view.getDefaultLabelWidth(label, paint) + padding;
+ final int width = (int)view.getLabelWidth(label, paint) + padding;
if (maxWidth < width) {
maxWidth = width;
}
diff --git a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
index 62a9259f9..347383f95 100644
--- a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
+++ b/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
@@ -17,12 +17,12 @@
package com.android.inputmethod.keyboard;
import android.content.Context;
-import android.os.Build;
import android.util.Log;
import android.view.MotionEvent;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Utils;
public class SuddenJumpingTouchEventHandler {
private static final String TAG = SuddenJumpingTouchEventHandler.class.getSimpleName();
@@ -49,18 +49,8 @@ public class SuddenJumpingTouchEventHandler {
public SuddenJumpingTouchEventHandler(Context context, ProcessMotionEvent view) {
mView = view;
- final String[] deviceList = context.getResources().getStringArray(
- R.array.sudden_jumping_touch_event_device_list);
- mNeedsSuddenJumpingHack = needsSuddenJumpingHack(Build.HARDWARE, deviceList);
- }
-
- private static boolean needsSuddenJumpingHack(String deviceName, String[] deviceList) {
- for (String device : deviceList) {
- if (device.equalsIgnoreCase(deviceName)) {
- return true;
- }
- }
- return false;
+ mNeedsSuddenJumpingHack = Boolean.parseBoolean(Utils.getDeviceOverrideValue(
+ context.getResources(), R.array.sudden_jumping_touch_event_device_list, "false"));
}
public void setKeyboard(Keyboard newKeyboard) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 31ff4e7b4..dfc8c8e9d 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -149,6 +149,9 @@ public class BinaryDictionary extends Dictionary {
int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize,
mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS,
MAX_PROXIMITY_CHARS_SIZE);
+ if (count > MAX_BIGRAMS) {
+ count = MAX_BIGRAMS;
+ }
for (int j = 0; j < count; ++j) {
if (codesSize > 0 && mBigramScores[j] < 1) break;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 9ffc7d0a2..8ec440500 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -196,8 +196,8 @@ public class BinaryDictionaryFileDumper {
} finally {
// Ignore exceptions while closing files.
try {
- // afd.close() will close inputStream, we should not call inputStream.close().
- if (null != afd) afd.close();
+ // inputStream.close() will close afd, we should not call afd.close().
+ if (null != inputStream) inputStream.close();
} catch (Exception e) {
Log.e(TAG, "Exception while closing a cross-process file descriptor : " + e);
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 99a4d54d8..e67f0ea05 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1288,7 +1288,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
mDeleteCount++;
mExpectingUpdateSelection = true;
mShouldSwitchToLastSubtype = true;
- LatinImeLogger.logOnDelete();
+ LatinImeLogger.logOnDelete(x, y);
break;
case Keyboard.CODE_SHIFT:
case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index 683dafa86..079f3b5dd 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -51,7 +51,7 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void logOnAutoCorrectionCancelled() {
}
- public static void logOnDelete() {
+ public static void logOnDelete(int x, int y) {
}
public static void logOnInputChar() {
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 9fc047b75..c1335fdfe 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -19,7 +19,6 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.os.Build;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
@@ -322,14 +321,8 @@ public class SettingsValues {
return volume;
}
- final String[] volumePerHardwareList = res.getStringArray(R.array.keypress_volumes);
- final String hardwarePrefix = Build.HARDWARE + ",";
- for (final String element : volumePerHardwareList) {
- if (element.startsWith(hardwarePrefix)) {
- return Float.parseFloat(element.substring(element.lastIndexOf(',') + 1));
- }
- }
- return -1.0f;
+ return Float.parseFloat(
+ Utils.getDeviceOverrideValue(res, R.array.keypress_volumes, "-1.0f"));
}
// Likewise
@@ -340,15 +333,9 @@ public class SettingsValues {
if (ms >= 0) {
return ms;
}
- final String[] durationPerHardwareList = res.getStringArray(
- R.array.keypress_vibration_durations);
- final String hardwarePrefix = Build.HARDWARE + ",";
- for (final String element : durationPerHardwareList) {
- if (element.startsWith(hardwarePrefix)) {
- return (int)Long.parseLong(element.substring(element.lastIndexOf(',') + 1));
- }
- }
- return -1;
+
+ return Integer.parseInt(
+ Utils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations, "-1"));
}
// Likewise
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index a7de47c58..e2ce08323 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -19,9 +19,11 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
@@ -43,6 +45,7 @@ import java.io.PrintWriter;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.HashMap;
public class Utils {
private Utils() {
@@ -260,8 +263,25 @@ public class Utils {
}
}
- public static void writeBackSpace() {
- UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0");
+ /**
+ * 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(
+ LogGroup.KEY, "<backspace>\t" + x + "\t" + y);
}
public void writeChar(char c, int x, int y) {
@@ -277,11 +297,12 @@ public class Utils {
inputChar = "<space>";
break;
}
- UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
+ UsabilityStudyLogUtils.getInstance().write(LogGroup.KEY,
+ inputChar + "\t" + x + "\t" + y);
LatinImeLogger.onPrintAllUsabilityStudyLogs();
}
- public void write(final String log) {
+ public void write(final LogGroup logGroup, final String log) {
mLoggingHandler.post(new Runnable() {
@Override
public void run() {
@@ -289,8 +310,8 @@ public class Utils {
final long currentTime = System.currentTimeMillis();
mDate.setTime(currentTime);
- final String printString = String.format("%s\t%d\t%s\n",
- mDateFormat.format(mDate), currentTime, log);
+ final String printString = String.format("%s\t%d\t%s\t%s\n",
+ mDateFormat.format(mDate), currentTime, logGroup.mLogString, log);
if (LatinImeLogger.sDBG) {
Log.d(USABILITY_TAG, "Write: " + log);
}
@@ -465,4 +486,23 @@ public class Utils {
if (TextUtils.isEmpty(info)) return null;
return info;
}
+
+ private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
+ private static final HashMap<Integer, String> sDeviceOverrideValueMap =
+ new HashMap<Integer, String>();
+
+ public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
+ final Integer key = overrideResId;
+ if (!sDeviceOverrideValueMap.containsKey(key)) {
+ String overrideValue = defValue;
+ for (final String element : res.getStringArray(overrideResId)) {
+ if (element.startsWith(HARDWARE_PREFIX)) {
+ overrideValue = element.substring(HARDWARE_PREFIX.length());
+ break;
+ }
+ }
+ sDeviceOverrideValueMap.put(key, overrideValue);
+ }
+ return sDeviceOverrideValueMap.get(key);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
new file mode 100644
index 000000000..42dd4df34
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -0,0 +1,1208 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public class BinaryDictInputOutput {
+
+ /* Node layout is as follows:
+ * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE
+ * 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
+ * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
+ * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
+ * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+ * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
+ * s | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
+ * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS
+ * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
+ * | is shortcut only ? 1 bit, 1 = yes, 0 = no : FLAG_IS_SHORTCUT_ONLY
+ *
+ * c | IF FLAG_HAS_MULTIPLE_CHARS
+ * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * a | end 1 byte, = 0
+ * r | ELSE
+ * s | char 1 or 3 bytes
+ * | END
+ *
+ * f |
+ * r | IF FLAG_IS_TERMINAL
+ * e | frequency 1 byte
+ * q |
+ *
+ * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
+ * h | // nothing
+ * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
+ * l | children address, 1 byte
+ * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
+ * r | children address, 2 bytes
+ * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
+ * n | children address, 3 bytes
+ * A | END
+ * d
+ * dress
+ *
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
+ * | shortcut targets address list
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+ * | bigrams address list
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * bigram and shortcut address list is:
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+ * | 1 = must take -address, 0 = must take +address
+ * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
+ * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
+ * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
+ * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+ * | read 1 byte, add top 4 bits
+ * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | read 2 bytes, add top 4 bits
+ * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+ * | read 3 bytes, add top 4 bits
+ * | END
+ * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
+ * if (FLAG_ATTRIBUTE_HAS_NET) goto bigram_and_shortcut_address_list_is
+ *
+ */
+
+ private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
+ private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+ private static final int MINIMUM_SUPPORTED_VERSION = 1;
+ private static final int MAXIMUM_SUPPORTED_VERSION = 2;
+ private static final int NOT_A_VERSION_NUMBER = -1;
+ private static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
+
+ // No options yet, reserved for future use.
+ private static final int OPTIONS = 0;
+
+ // TODO: Make this value adaptative to content data, store it in the header, and
+ // use it in the reading code.
+ private static final int MAX_WORD_LENGTH = 48;
+
+ private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+ private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+ private static final int FLAG_IS_TERMINAL = 0x10;
+ private static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
+ private static final int FLAG_HAS_BIGRAMS = 0x04;
+ private static final int FLAG_IS_SHORTCUT_ONLY = 0x02;
+
+ private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+ private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+ private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+ private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+
+ private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+ private static final int GROUP_TERMINATOR_SIZE = 1;
+ private static final int GROUP_FLAGS_SIZE = 1;
+ private static final int GROUP_FREQUENCY_SIZE = 1;
+ private static final int GROUP_MAX_ADDRESS_SIZE = 3;
+ private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
+ private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+
+ private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
+ private static final int INVALID_CHARACTER = -1;
+
+ private static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
+ private static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
+
+ private static final int MAX_TERMINAL_FREQUENCY = 255;
+
+ /**
+ * A class grouping utility function for our specific character encoding.
+ */
+ private static class CharEncoding {
+
+ private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+ private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
+
+ /**
+ * Helper method to find out whether this code fits on one byte
+ */
+ private static boolean fitsOnOneByte(int character) {
+ return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
+ && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
+ }
+
+ /**
+ * Compute the size of a character given its character code.
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * @param character the character code.
+ * @return the size in binary encoded-form, either 1 or 3 bytes.
+ */
+ private static int getCharSize(int character) {
+ // See char encoding in FusionDictionary.java
+ if (fitsOnOneByte(character)) return 1;
+ if (INVALID_CHARACTER == character) return 1;
+ return 3;
+ }
+
+ /**
+ * Compute the byte size of a character array.
+ */
+ private static int getCharArraySize(final int[] chars) {
+ int size = 0;
+ for (int character : chars) size += getCharSize(character);
+ return size;
+ }
+
+ /**
+ * Writes a char array to a byte buffer.
+ *
+ * @param characters the character array to write.
+ * @param buffer the byte buffer to write to.
+ * @param index the index in buffer to write the character array to.
+ * @return the index after the last character.
+ */
+ private static int writeCharArray(int[] characters, byte[] buffer, int index) {
+ for (int character : characters) {
+ if (1 == getCharSize(character)) {
+ buffer[index++] = (byte)character;
+ } else {
+ buffer[index++] = (byte)(0xFF & (character >> 16));
+ buffer[index++] = (byte)(0xFF & (character >> 8));
+ buffer[index++] = (byte)(0xFF & character);
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Reads a character from the file.
+ *
+ * This follows the character format documented earlier in this source file.
+ *
+ * @param source the file, positioned over an encoded character.
+ * @return the character code.
+ */
+ private static int readChar(RandomAccessFile source) throws IOException {
+ int character = source.readUnsignedByte();
+ if (!fitsOnOneByte(character)) {
+ if (GROUP_CHARACTERS_TERMINATOR == character)
+ return INVALID_CHARACTER;
+ character <<= 16;
+ character += source.readUnsignedShort();
+ }
+ return character;
+ }
+ }
+
+ /**
+ * Compute the binary size of the character array in a group
+ *
+ * If only one character, this is the size of this character. If many, it's the sum of their
+ * sizes + 1 byte for the terminator.
+ *
+ * @param group the group
+ * @return the size of the char array, including the terminator if any
+ */
+ private static int getGroupCharactersSize(CharGroup group) {
+ int size = CharEncoding.getCharArraySize(group.mChars);
+ if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the binary size of the group count
+ * @param count the group count
+ * @return the size of the group count, either 1 or 2 bytes.
+ */
+ private static int getGroupCountSize(final int count) {
+ if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
+ return 1;
+ } else if (MAX_CHARGROUPS_IN_A_NODE >= count) {
+ return 2;
+ } else {
+ throw new RuntimeException("Can't have more than " + MAX_CHARGROUPS_IN_A_NODE
+ + " groups in a node (found " + count +")");
+ }
+ }
+
+ /**
+ * Compute the binary size of the group count for a node
+ * @param node the node
+ * @return the size of the group count, either 1 or 2 bytes.
+ */
+ private static int getGroupCountSize(final Node node) {
+ return getGroupCountSize(node.mData.size());
+ }
+
+ /**
+ * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
+ *
+ * @param group the CharGroup to compute the size of.
+ * @return the maximum size of the group.
+ */
+ private static int getCharGroupMaximumSize(CharGroup group) {
+ int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+ // If terminal, one byte for the frequency
+ if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE;
+ size += GROUP_MAX_ADDRESS_SIZE; // For children address
+ if (null != group.mShortcutTargets) {
+ size += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
+ * group.mShortcutTargets.size();
+ }
+ if (null != group.mBigrams) {
+ size += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
+ * group.mBigrams.size();
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
+ * it in the 'actualSize' member of the node.
+ *
+ * @param node the node to compute the maximum size of.
+ */
+ private static void setNodeMaximumSize(Node node) {
+ int size = getGroupCountSize(node);
+ for (CharGroup g : node.mData) {
+ final int groupSize = getCharGroupMaximumSize(g);
+ g.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ node.mCachedSize = size;
+ }
+
+ /**
+ * Helper method to hide the actual value of the no children address.
+ */
+ private static boolean hasChildrenAddress(int address) {
+ return NO_CHILDREN_ADDRESS != address;
+ }
+
+ /**
+ * Helper method to find out if a character info is a shortcut only.
+ */
+ private static boolean isShortcutOnly(final CharGroupInfo info) {
+ return 0 != (info.mFlags & FLAG_IS_SHORTCUT_ONLY);
+ }
+
+ /**
+ * Compute the size, in bytes, that an address will occupy.
+ *
+ * This can be used either for children addresses (which are always positive) or for
+ * attribute, which may be positive or negative but
+ * store their sign bit separately.
+ *
+ * @param address the address
+ * @return the byte size.
+ */
+ private static int getByteSize(int address) {
+ assert(address < 0x1000000);
+ if (!hasChildrenAddress(address)) {
+ return 0;
+ } else if (Math.abs(address) < 0x100) {
+ return 1;
+ } else if (Math.abs(address) < 0x10000) {
+ return 2;
+ } else {
+ return 3;
+ }
+ }
+ // End utility methods.
+
+ // This method is responsible for finding a nice ordering of the nodes that favors run-time
+ // cache performance and dictionary size.
+ /* package for tests */ static ArrayList<Node> flattenTree(Node root) {
+ final int treeSize = FusionDictionary.countCharGroups(root);
+ MakedictLog.i("Counted nodes : " + treeSize);
+ final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
+ return flattenTreeInner(flatTree, root);
+ }
+
+ private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) {
+ // Removing the node is necessary if the tails are merged, because we would then
+ // add the same node several times when we only want it once. A number of places in
+ // the code also depends on any node being only once in the list.
+ // Merging tails can only be done if there are no attributes. Searching for attributes
+ // in LatinIME code depends on a total breadth-first ordering, which merging tails
+ // breaks. If there are no attributes, it should be fine (and reduce the file size)
+ // to merge tails, and the following step would be necessary.
+ // If eventually the code runs on Android, searching through the whole array each time
+ // may be a performance concern.
+ list.remove(node);
+ list.add(node);
+ final ArrayList<CharGroup> branches = node.mData;
+ final int nodeSize = branches.size();
+ for (CharGroup group : branches) {
+ if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
+ }
+ return list;
+ }
+
+ /**
+ * Finds the absolute address of a word in the dictionary.
+ *
+ * @param dict the dictionary in which to search.
+ * @param word the word we are searching for.
+ * @return the word address. If it is not found, an exception is thrown.
+ */
+ private static int findAddressOfWord(final FusionDictionary dict, final String word) {
+ return FusionDictionary.findWordInTree(dict.mRoot, word).mCachedAddress;
+ }
+
+ /**
+ * Computes the actual node size, based on the cached addresses of the children nodes.
+ *
+ * Each node stores its tentative address. During dictionary address computing, these
+ * are not final, but they can be used to compute the node size (the node size depends
+ * on the address of the children because the number of bytes necessary to store an
+ * address depends on its numeric value.
+ *
+ * @param node the node to compute the size of.
+ * @param dict the dictionary in which the word/attributes are to be found.
+ */
+ private static void computeActualNodeSize(Node node, FusionDictionary dict) {
+ int size = getGroupCountSize(node);
+ for (CharGroup group : node.mData) {
+ int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
+ if (null != group.mChildren) {
+ final int offsetBasePoint= groupSize + node.mCachedAddress + size;
+ final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
+ groupSize += getByteSize(offset);
+ }
+ if (null != group.mShortcutTargets) {
+ for (WeightedString target : group.mShortcutTargets) {
+ final int offsetBasePoint = groupSize + node.mCachedAddress + size
+ + GROUP_FLAGS_SIZE;
+ final int addressOfTarget = findAddressOfWord(dict, target.mWord);
+ final int offset = addressOfTarget - offsetBasePoint;
+ groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+ }
+ }
+ if (null != group.mBigrams) {
+ for (WeightedString bigram : group.mBigrams) {
+ final int offsetBasePoint = groupSize + node.mCachedAddress + size
+ + GROUP_FLAGS_SIZE;
+ final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+ final int offset = addressOfBigram - offsetBasePoint;
+ groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+ }
+ }
+ group.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ node.mCachedSize = size;
+ }
+
+ /**
+ * Computes the byte size of a list of nodes and updates each node cached position.
+ *
+ * @param flatNodes the array of nodes.
+ * @return the byte size of the entire stack.
+ */
+ private static int stackNodes(ArrayList<Node> flatNodes) {
+ int nodeOffset = 0;
+ for (Node n : flatNodes) {
+ n.mCachedAddress = nodeOffset;
+ int groupCountSize = getGroupCountSize(n);
+ int groupOffset = 0;
+ for (CharGroup g : n.mData) {
+ g.mCachedAddress = groupCountSize + nodeOffset + groupOffset;
+ groupOffset += g.mCachedSize;
+ }
+ if (groupOffset + groupCountSize != n.mCachedSize) {
+ throw new RuntimeException("Bug : Stored and computed node size differ");
+ }
+ nodeOffset += n.mCachedSize;
+ }
+ return nodeOffset;
+ }
+
+ /**
+ * Compute the addresses and sizes of an ordered node array.
+ *
+ * This method takes a node array and will update its cached address and size values
+ * so that they can be written into a file. It determines the smallest size each of the
+ * nodes can be given the addresses of its children and attributes, and store that into
+ * each node.
+ * The order of the node is given by the order of the array. This method makes no effort
+ * to find a good order; it only mechanically computes the size this order results in.
+ *
+ * @param dict the dictionary
+ * @param flatNodes the ordered array of nodes
+ * @return the same array it was passed. The nodes have been updated for address and size.
+ */
+ private static ArrayList<Node> computeAddresses(FusionDictionary dict,
+ ArrayList<Node> flatNodes) {
+ // First get the worst sizes and offsets
+ for (Node n : flatNodes) setNodeMaximumSize(n);
+ final int offset = stackNodes(flatNodes);
+
+ MakedictLog.i("Compressing the array addresses. Original size : " + offset);
+ MakedictLog.i("(Recursively seen size : " + offset + ")");
+
+ int passes = 0;
+ boolean changesDone = false;
+ do {
+ changesDone = false;
+ for (Node n : flatNodes) {
+ final int oldNodeSize = n.mCachedSize;
+ computeActualNodeSize(n, dict);
+ final int newNodeSize = n.mCachedSize;
+ if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
+ if (oldNodeSize != newNodeSize) changesDone = true;
+ }
+ stackNodes(flatNodes);
+ ++passes;
+ } while (changesDone);
+
+ final Node lastNode = flatNodes.get(flatNodes.size() - 1);
+ MakedictLog.i("Compression complete in " + passes + " passes.");
+ MakedictLog.i("After address compression : "
+ + (lastNode.mCachedAddress + lastNode.mCachedSize));
+
+ return flatNodes;
+ }
+
+ /**
+ * Sanity-checking method.
+ *
+ * This method checks an array of node for juxtaposition, that is, it will do
+ * nothing if each node's cached address is actually the previous node's address
+ * plus the previous node's size.
+ * If this is not the case, it will throw an exception.
+ *
+ * @param array the array node to check
+ */
+ private static void checkFlatNodeArray(ArrayList<Node> array) {
+ int offset = 0;
+ int index = 0;
+ for (Node n : array) {
+ if (n.mCachedAddress != offset) {
+ throw new RuntimeException("Wrong address for node " + index
+ + " : expected " + offset + ", got " + n.mCachedAddress);
+ }
+ ++index;
+ offset += n.mCachedSize;
+ }
+ }
+
+ /**
+ * Helper method to write a variable-size address to a file.
+ *
+ * @param buffer the buffer to write to.
+ * @param index the index in the buffer to write the address to.
+ * @param address the address to write.
+ * @return the size in bytes the address actually took.
+ */
+ private static int writeVariableAddress(byte[] buffer, int index, int address) {
+ switch (getByteSize(address)) {
+ case 1:
+ buffer[index++] = (byte)address;
+ return 1;
+ case 2:
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 2;
+ case 3:
+ buffer[index++] = (byte)(0xFF & (address >> 16));
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 3;
+ case 0:
+ return 0;
+ default:
+ throw new RuntimeException("Address " + address + " has a strange size");
+ }
+ }
+
+ private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
+ final int childrenOffset) {
+ byte flags = 0;
+ if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+ if (group.mFrequency >= 0) {
+ flags |= FLAG_IS_TERMINAL;
+ }
+ if (null != group.mChildren) {
+ switch (getByteSize(childrenOffset)) {
+ case 1:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Node with a strange address");
+ }
+ }
+ if (null != group.mShortcutTargets) {
+ if (0 == group.mShortcutTargets.size()) {
+ throw new RuntimeException("0-sized shortcut list must be null");
+ }
+ flags |= FLAG_HAS_SHORTCUT_TARGETS;
+ }
+ if (null != group.mBigrams) {
+ if (0 == group.mBigrams.size()) {
+ throw new RuntimeException("0-sized bigram list must be null");
+ }
+ flags |= FLAG_HAS_BIGRAMS;
+ }
+ if (group.mIsShortcutOnly) {
+ flags |= FLAG_IS_SHORTCUT_ONLY;
+ }
+ return flags;
+ }
+
+ /**
+ * Makes the flag value for an attribute.
+ *
+ * @param more whether there are more attributes after this one.
+ * @param offset the offset of the attribute.
+ * @param frequency the frequency of the attribute, 0..15
+ * @return the flags
+ */
+ private static final int makeAttributeFlags(final boolean more, final int offset,
+ final int frequency) {
+ int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
+ + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+ switch (getByteSize(offset)) {
+ case 1:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Strange offset size");
+ }
+ bigramFlags += frequency & FLAG_ATTRIBUTE_FREQUENCY;
+ return bigramFlags;
+ }
+
+ /**
+ * Write a node to memory. The node is expected to have its final position cached.
+ *
+ * This can be an empty map, but the more is inside the faster the lookups will be. It can
+ * be carried on as long as nodes do not move.
+ *
+ * @param dict the dictionary the node is a part of (for relative offsets).
+ * @param buffer the memory buffer to write to.
+ * @param node the node to write.
+ * @return the address of the END of the node.
+ */
+ private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+ int index = node.mCachedAddress;
+
+ final int groupCount = node.mData.size();
+ final int countSize = getGroupCountSize(node);
+ if (1 == countSize) {
+ buffer[index++] = (byte)groupCount;
+ } else if (2 == countSize) {
+ // We need to signal 2-byte size by setting the top bit of the MSB to 1, so
+ // we | 0x80 to do this.
+ buffer[index++] = (byte)((groupCount >> 8) | 0x80);
+ buffer[index++] = (byte)(groupCount & 0xFF);
+ } else {
+ throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
+ }
+ int groupAddress = index;
+ for (int i = 0; i < groupCount; ++i) {
+ CharGroup group = node.mData.get(i);
+ if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
+ + "the same as the cached address of the group");
+ groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ // Sanity checks.
+ if (group.mFrequency > MAX_TERMINAL_FREQUENCY) {
+ throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+ + " : " + group.mFrequency);
+ }
+ if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE;
+ final int childrenOffset = null == group.mChildren
+ ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
+ byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
+ buffer[index++] = flags;
+ index = CharEncoding.writeCharArray(group.mChars, buffer, index);
+ if (group.hasSeveralChars()) {
+ buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+ }
+ if (group.mFrequency >= 0) {
+ buffer[index++] = (byte) group.mFrequency;
+ }
+ final int shift = writeVariableAddress(buffer, index, childrenOffset);
+ index += shift;
+ groupAddress += shift;
+
+ // Write shortcuts
+ if (null != group.mShortcutTargets) {
+ final Iterator shortcutIterator = group.mShortcutTargets.iterator();
+ while (shortcutIterator.hasNext()) {
+ final WeightedString target = (WeightedString)shortcutIterator.next();
+ final int addressOfTarget = findAddressOfWord(dict, target.mWord);
+ ++groupAddress;
+ final int offset = addressOfTarget - groupAddress;
+ int shortcutFlags = makeAttributeFlags(shortcutIterator.hasNext(), offset,
+ target.mFrequency);
+ buffer[index++] = (byte)shortcutFlags;
+ final int shortcutShift = writeVariableAddress(buffer, index, Math.abs(offset));
+ index += shortcutShift;
+ groupAddress += shortcutShift;
+ }
+ }
+ // Write bigrams
+ if (null != group.mBigrams) {
+ final Iterator bigramIterator = group.mBigrams.iterator();
+ while (bigramIterator.hasNext()) {
+ final WeightedString bigram = (WeightedString)bigramIterator.next();
+ final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+ ++groupAddress;
+ final int offset = addressOfBigram - groupAddress;
+ int bigramFlags = makeAttributeFlags(bigramIterator.hasNext(), offset,
+ bigram.mFrequency);
+ buffer[index++] = (byte)bigramFlags;
+ final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
+ index += bigramShift;
+ groupAddress += bigramShift;
+ }
+ }
+
+ }
+ if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException(
+ "Not the same size : written "
+ + (index - node.mCachedAddress) + " bytes out of a node that should have "
+ + node.mCachedSize + " bytes");
+ return index;
+ }
+
+ /**
+ * Dumps a collection of useful statistics about a node array.
+ *
+ * This prints purely informative stuff, like the total estimated file size, the
+ * number of nodes, of character groups, the repartition of each address size, etc
+ *
+ * @param nodes the node array.
+ */
+ private static void showStatistics(ArrayList<Node> nodes) {
+ int firstTerminalAddress = Integer.MAX_VALUE;
+ int lastTerminalAddress = Integer.MIN_VALUE;
+ int size = 0;
+ int charGroups = 0;
+ int maxGroups = 0;
+ int maxRuns = 0;
+ for (Node n : nodes) {
+ if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
+ for (CharGroup cg : n.mData) {
+ ++charGroups;
+ if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
+ if (cg.mFrequency >= 0) {
+ if (n.mCachedAddress < firstTerminalAddress)
+ firstTerminalAddress = n.mCachedAddress;
+ if (n.mCachedAddress > lastTerminalAddress)
+ lastTerminalAddress = n.mCachedAddress;
+ }
+ }
+ if (n.mCachedAddress + n.mCachedSize > size) size = n.mCachedAddress + n.mCachedSize;
+ }
+ final int[] groupCounts = new int[maxGroups + 1];
+ final int[] runCounts = new int[maxRuns + 1];
+ for (Node n : nodes) {
+ ++groupCounts[n.mData.size()];
+ for (CharGroup cg : n.mData) {
+ ++runCounts[cg.mChars.length];
+ }
+ }
+
+ MakedictLog.i("Statistics:\n"
+ + " total file size " + size + "\n"
+ + " " + nodes.size() + " nodes\n"
+ + " " + charGroups + " groups (" + ((float)charGroups / nodes.size())
+ + " groups per node)\n"
+ + " first terminal at " + firstTerminalAddress + "\n"
+ + " last terminal at " + lastTerminalAddress + "\n"
+ + " Group stats : max = " + maxGroups);
+ for (int i = 0; i < groupCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + groupCounts[i]);
+ }
+ MakedictLog.i(" Character run stats : max = " + maxRuns);
+ for (int i = 0; i < runCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + runCounts[i]);
+ }
+ }
+
+ /**
+ * Dumps a FusionDictionary to a file.
+ *
+ * This is the public entry point to write a dictionary to a file.
+ *
+ * @param destination the stream to write the binary data to.
+ * @param dict the dictionary to write.
+ * @param version the version of the format to write, currently either 1 or 2.
+ */
+ public static void writeDictionaryBinary(final OutputStream destination,
+ final FusionDictionary dict, final int version)
+ throws IOException, UnsupportedFormatException {
+
+ // Addresses are limited to 3 bytes, so we'll just make a 16MB buffer. Since addresses
+ // can be relative to each node, the structure itself is not limited to 16MB at all, but
+ // I doubt this will ever be shot. If it is, deciding the order of the nodes becomes
+ // a quite complicated problem, because though the dictionary itself does not have a
+ // size limit, each node must still be within 16MB of all its children and parents.
+ // As long as this is ensured, the dictionary file may grow to any size.
+ // Anyway, to make a dictionary bigger than 16MB just increase the size of this buffer.
+ final byte[] buffer = new byte[1 << 24];
+ int index = 0;
+
+ if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
+ throw new UnsupportedFormatException("Requested file format version " + version
+ + ", but this implementation only supports versions "
+ + MINIMUM_SUPPORTED_VERSION + " through " + MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ // The magic number in big-endian order.
+ if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ // Magic number for version 2+.
+ buffer[index++] = (byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 24));
+ buffer[index++] = (byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 16));
+ buffer[index++] = (byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 8));
+ buffer[index++] = (byte) (0xFF & VERSION_2_MAGIC_NUMBER);
+ // Dictionary version.
+ buffer[index++] = (byte) (0xFF & (version >> 8));
+ buffer[index++] = (byte) (0xFF & version);
+ } else {
+ // Magic number for version 1.
+ buffer[index++] = (byte) (0xFF & (VERSION_1_MAGIC_NUMBER >> 8));
+ buffer[index++] = (byte) (0xFF & VERSION_1_MAGIC_NUMBER);
+ // Dictionary version.
+ buffer[index++] = (byte) (0xFF & version);
+ }
+ // Options flags
+ buffer[index++] = (byte) (0xFF & (OPTIONS >> 8));
+ buffer[index++] = (byte) (0xFF & OPTIONS);
+ if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ final int headerSizeOffset = index;
+ index += 4; // Size of the header size
+ // TODO: Write out the header contents here.
+ // Write out the header size.
+ buffer[headerSizeOffset] = (byte) (0xFF & (index >> 24));
+ buffer[headerSizeOffset + 1] = (byte) (0xFF & (index >> 16));
+ buffer[headerSizeOffset + 2] = (byte) (0xFF & (index >> 8));
+ buffer[headerSizeOffset + 3] = (byte) (0xFF & (index >> 0));
+ }
+
+ destination.write(buffer, 0, index);
+ index = 0;
+
+ // Leave the choice of the optimal node order to the flattenTree function.
+ MakedictLog.i("Flattening the tree...");
+ ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
+
+ MakedictLog.i("Computing addresses...");
+ computeAddresses(dict, flatNodes);
+ MakedictLog.i("Checking array...");
+ checkFlatNodeArray(flatNodes);
+
+ MakedictLog.i("Writing file...");
+ int dataEndOffset = 0;
+ for (Node n : flatNodes) {
+ dataEndOffset = writePlacedNode(dict, buffer, n);
+ }
+
+ showStatistics(flatNodes);
+
+ destination.write(buffer, 0, dataEndOffset);
+
+ destination.close();
+ MakedictLog.i("Done");
+ }
+
+
+ // Input methods: Read a binary dictionary to memory.
+ // readDictionaryBinary is the public entry point for them.
+
+ static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
+ private static CharGroupInfo readCharGroup(RandomAccessFile source,
+ final int originalGroupAddress) throws IOException {
+ int addressPointer = originalGroupAddress;
+ final int flags = source.readUnsignedByte();
+ ++addressPointer;
+ final int characters[];
+ if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+ int index = 0;
+ int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ while (-1 != character) {
+ characterBuffer[index++] = character;
+ character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ }
+ characters = Arrays.copyOfRange(characterBuffer, 0, index);
+ } else {
+ final int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ characters = new int[] { character };
+ }
+ final int frequency;
+ if (0 != (FLAG_IS_TERMINAL & flags)) {
+ ++addressPointer;
+ frequency = source.readUnsignedByte();
+ } else {
+ frequency = CharGroup.NOT_A_TERMINAL;
+ }
+ int childrenAddress = addressPointer;
+ switch (flags & MASK_GROUP_ADDRESS_TYPE) {
+ case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+ childrenAddress += source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+ childrenAddress += source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+ childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+ addressPointer += 3;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
+ default:
+ childrenAddress = NO_CHILDREN_ADDRESS;
+ break;
+ }
+ ArrayList<PendingAttribute> shortcutTargets = null;
+ if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) {
+ shortcutTargets = new ArrayList<PendingAttribute>();
+ while (true) {
+ final int targetFlags = source.readUnsignedByte();
+ ++addressPointer;
+ final int sign = 0 == (targetFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+ int targetAddress = addressPointer;
+ switch (targetFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+ targetAddress += sign * source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+ targetAddress += sign * source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+ final int offset = ((source.readUnsignedByte() << 16)
+ + source.readUnsignedShort());
+ targetAddress += sign * offset;
+ addressPointer += 3;
+ break;
+ default:
+ throw new RuntimeException("Has shortcut targets with no address");
+ }
+ shortcutTargets.add(new PendingAttribute(targetFlags & FLAG_ATTRIBUTE_FREQUENCY,
+ targetAddress));
+ if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ }
+ }
+ ArrayList<PendingAttribute> bigrams = null;
+ if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+ bigrams = new ArrayList<PendingAttribute>();
+ while (true) {
+ final int bigramFlags = source.readUnsignedByte();
+ ++addressPointer;
+ final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+ int bigramAddress = addressPointer;
+ switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+ bigramAddress += sign * source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+ bigramAddress += sign * source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+ final int offset = ((source.readUnsignedByte() << 16)
+ + source.readUnsignedShort());
+ bigramAddress += sign * offset;
+ addressPointer += 3;
+ break;
+ default:
+ throw new RuntimeException("Has bigrams with no address");
+ }
+ bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY,
+ bigramAddress));
+ if (0 == (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ }
+ }
+ return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
+ childrenAddress, shortcutTargets, bigrams);
+ }
+
+ /**
+ * Reads and returns the char group count out of a file and forwards the pointer.
+ */
+ private static int readCharGroupCount(RandomAccessFile source) throws IOException {
+ final int msb = source.readUnsignedByte();
+ if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
+ return msb;
+ } else {
+ return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
+ + source.readUnsignedByte();
+ }
+ }
+
+ /**
+ * Finds, as a string, the word at the address passed as an argument.
+ *
+ * @param source the file to read from.
+ * @param headerSize the size of the header.
+ * @param address the address to seek.
+ * @return the word, as a string.
+ * @throws IOException if the file can't be read.
+ */
+ private static String getWordAtAddress(RandomAccessFile source, long headerSize,
+ int address) throws IOException {
+ final long originalPointer = source.getFilePointer();
+ source.seek(headerSize);
+ final int count = readCharGroupCount(source);
+ int groupOffset = getGroupCountSize(count);
+ final StringBuilder builder = new StringBuilder();
+ String result = null;
+
+ CharGroupInfo last = null;
+ for (int i = count - 1; i >= 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ groupOffset = info.mEndAddress;
+ if (info.mOriginalAddress == address) {
+ builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
+ result = builder.toString();
+ break; // and return
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ if (info.mChildrenAddress > address) {
+ if (null == last) continue;
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ last = info;
+ }
+ if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ }
+ source.seek(originalPointer);
+ return result;
+ }
+
+ /**
+ * Reads a single node from a binary file.
+ *
+ * This methods reads the file at the current position of its file pointer. A node is
+ * fully expected to start at the current position.
+ * This will recursively read other nodes into the structure, populating the reverse
+ * maps on the fly and using them to keep track of already read nodes.
+ *
+ * @param source the data file, correctly positioned at the start of a node.
+ * @param headerSize the size, in bytes, of the file header.
+ * @param reverseNodeMap a mapping from addresses to already read nodes.
+ * @param reverseGroupMap a mapping from addresses to already read character groups.
+ * @return the read node with all his children already read.
+ */
+ private static Node readNode(RandomAccessFile source, long headerSize,
+ Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+ throws IOException {
+ final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
+ final int count = readCharGroupCount(source);
+ final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
+ int groupOffset = nodeOrigin + getGroupCountSize(count);
+ for (int i = count; i > 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ ArrayList<WeightedString> shortcutTargets = null;
+ if (null != info.mShortcutTargets) {
+ shortcutTargets = new ArrayList<WeightedString>();
+ for (PendingAttribute target : info.mShortcutTargets) {
+ final String word = getWordAtAddress(source, headerSize, target.mAddress);
+ shortcutTargets.add(new WeightedString(word, target.mFrequency));
+ }
+ }
+ ArrayList<WeightedString> bigrams = null;
+ if (null != info.mBigrams) {
+ bigrams = new ArrayList<WeightedString>();
+ for (PendingAttribute bigram : info.mBigrams) {
+ final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
+ bigrams.add(new WeightedString(word, bigram.mFrequency));
+ }
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ Node children = reverseNodeMap.get(info.mChildrenAddress);
+ if (null == children) {
+ final long currentPosition = source.getFilePointer();
+ source.seek(info.mChildrenAddress + headerSize);
+ children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
+ source.seek(currentPosition);
+ }
+ nodeContents.add(
+ new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
+ children, isShortcutOnly(info)));
+ } else {
+ nodeContents.add(
+ new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
+ isShortcutOnly(info)));
+ }
+ groupOffset = info.mEndAddress;
+ }
+ final Node node = new Node(nodeContents);
+ node.mCachedAddress = nodeOrigin;
+ reverseNodeMap.put(node.mCachedAddress, node);
+ return node;
+ }
+
+ /**
+ * Helper function to get the binary format version from the header.
+ */
+ private static int getFormatVersion(final RandomAccessFile source) throws IOException {
+ final int magic_v1 = source.readUnsignedShort();
+ if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte();
+ final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort();
+ if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort();
+ return NOT_A_VERSION_NUMBER;
+ }
+
+ /**
+ * Reads a random access file and returns the memory representation of the dictionary.
+ *
+ * This high-level method takes a binary file and reads its contents, populating a
+ * FusionDictionary structure. The optional dict argument is an existing dictionary to
+ * which words from the file should be added. If it is null, a new dictionary is created.
+ *
+ * @param source the file to read.
+ * @param dict an optional dictionary to add words to, or null.
+ * @return the created (or merged) dictionary.
+ */
+ public static FusionDictionary readDictionaryBinary(final RandomAccessFile source,
+ final FusionDictionary dict) throws IOException, UnsupportedFormatException {
+ // Check file version
+ final int version = getFormatVersion(source);
+ if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) {
+ throw new UnsupportedFormatException("This file has version " + version
+ + ", but this implementation does not support versions above "
+ + MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ // Read options
+ source.readUnsignedShort();
+
+ final long headerSize;
+ if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
+ headerSize = source.getFilePointer();
+ } else {
+ headerSize = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16)
+ + (source.readUnsignedByte() << 8) + source.readUnsignedByte();
+ // read the header body
+ source.seek(headerSize);
+ }
+
+ Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
+ Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
+ final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+
+ FusionDictionary newDict = new FusionDictionary(root,
+ new FusionDictionary.DictionaryOptions());
+ if (null != dict) {
+ for (Word w : dict) {
+ newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mBigrams);
+ }
+ }
+
+ return newDict;
+ }
+
+ /**
+ * Basic test to find out whether the file is a binary dictionary or not.
+ *
+ * Concretely this only tests the magic number.
+ *
+ * @param filename The name of the file to test.
+ * @return true if it's a binary dictionary, false otherwise
+ */
+ public static boolean isBinaryDictionary(final String filename) {
+ try {
+ RandomAccessFile f = new RandomAccessFile(filename, "r");
+ final int version = getFormatVersion(f);
+ return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
new file mode 100644
index 000000000..444b11732
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+import java.util.ArrayList;
+
+/**
+ * Raw char group info straight out of a file. This will contain numbers for addresses.
+ */
+public class CharGroupInfo {
+
+ public final int mOriginalAddress;
+ public final int mEndAddress;
+ public final int mFlags;
+ public final int[] mCharacters;
+ public final int mFrequency;
+ public final int mChildrenAddress;
+ public final ArrayList<PendingAttribute> mShortcutTargets;
+ public final ArrayList<PendingAttribute> mBigrams;
+
+ public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+ final int[] characters, final int frequency, final int childrenAddress,
+ final ArrayList<PendingAttribute> shortcutTargets,
+ final ArrayList<PendingAttribute> bigrams) {
+ mOriginalAddress = originalAddress;
+ mEndAddress = endAddress;
+ mFlags = flags;
+ mCharacters = characters;
+ mFrequency = frequency;
+ mChildrenAddress = childrenAddress;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
new file mode 100644
index 000000000..d19672181
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A dictionary that can fusion heads and tails of words for more compression.
+ */
+public class FusionDictionary implements Iterable<Word> {
+
+ /**
+ * A node of the dictionary, containing several CharGroups.
+ *
+ * A node is but an ordered array of CharGroups, which essentially contain all the
+ * real information.
+ * This class also contains fields to cache size and address, to help with binary
+ * generation.
+ */
+ public static class Node {
+ ArrayList<CharGroup> mData;
+ // To help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+ public Node() {
+ mData = new ArrayList<CharGroup>();
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ public Node(ArrayList<CharGroup> data) {
+ mData = data;
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * A string with a frequency.
+ *
+ * This represents an "attribute", that is either a bigram or a shortcut.
+ */
+ public static class WeightedString {
+ final String mWord;
+ final int mFrequency;
+ public WeightedString(String word, int frequency) {
+ mWord = word;
+ mFrequency = frequency;
+ }
+ }
+
+ /**
+ * A group of characters, with a frequency, shortcut targets, bigrams, and children.
+ *
+ * This is the central class of the in-memory representation. A CharGroup is what can
+ * be seen as a traditional "trie node", except it can hold several characters at the
+ * same time. A CharGroup essentially represents one or several characters in the middle
+ * of the trie trie; as such, it can be a terminal, and it can have children.
+ * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+ * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other
+ * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or
+ * bigrams, but a non-terminal may not. Moreover, children, if present, are null.
+ */
+ 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.
+ Node mChildren;
+ // The two following members to help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+
+ public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams, final int frequency,
+ final boolean isShortcutOnly) {
+ mChars = chars;
+ mFrequency = frequency;
+ mIsShortcutOnly = isShortcutOnly;
+ if (mIsShortcutOnly && NOT_A_TERMINAL == mFrequency) {
+ throw new RuntimeException("A node must be a terminal to be a shortcut only");
+ }
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ mChildren = null;
+ }
+
+ public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams, final int frequency, final Node children,
+ final boolean isShortcutOnly) {
+ mChars = chars;
+ mFrequency = frequency;
+ mIsShortcutOnly = isShortcutOnly;
+ if (mIsShortcutOnly && NOT_A_TERMINAL == mFrequency) {
+ throw new RuntimeException("A node must be a terminal to be a shortcut only");
+ }
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ mChildren = children;
+ }
+
+ public void addChild(CharGroup n) {
+ if (null == mChildren) {
+ mChildren = new Node();
+ }
+ mChildren.mData.add(n);
+ }
+
+ public boolean isTerminal() {
+ return NOT_A_TERMINAL != mFrequency;
+ }
+
+ public boolean hasSeveralChars() {
+ assert(mChars.length > 0);
+ return 1 < mChars.length;
+ }
+ }
+
+ /**
+ * Options global to the dictionary.
+ *
+ * There are no options at the moment, so this class is empty.
+ */
+ public static class DictionaryOptions {
+ }
+
+
+ public final DictionaryOptions mOptions;
+ public final Node mRoot;
+
+ public FusionDictionary() {
+ mOptions = new DictionaryOptions();
+ mRoot = new Node();
+ }
+
+ public FusionDictionary(final Node root, final DictionaryOptions options) {
+ mRoot = root;
+ mOptions = options;
+ }
+
+ /**
+ * Helper method to convert a String to an int array.
+ */
+ static private int[] getCodePoints(String word) {
+ final int wordLength = word.length();
+ int[] array = new int[word.codePointCount(0, wordLength)];
+ for (int i = 0; i < wordLength; i = word.offsetByCodePoints(i, 1)) {
+ array[i] = word.codePointAt(i);
+ }
+ return array;
+ }
+
+ /**
+ * Helper method to add all words in a list as 0-frequency entries
+ *
+ * These words are added when shortcuts targets or bigrams are not found in the dictionary
+ * yet. The same words may be added later with an actual frequency - this is handled by
+ * the private version of add().
+ */
+ private void addNeutralWords(final ArrayList<WeightedString> words) {
+ if (null != words) {
+ for (WeightedString word : words) {
+ final CharGroup t = findWordInTree(mRoot, word.mWord);
+ if (null == t) {
+ add(getCodePoints(word.mWord), 0, null, null, false /* isShortcutOnly */);
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to add a word as a string.
+ *
+ * This method adds a word to the dictionary with the given frequency. Optional
+ * lists of bigrams and shortcuts can be passed here. For each word inside,
+ * they will be added to the dictionary as necessary.
+ *
+ * @param word the word to add.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param shortcutTargets a list of shortcut targets for this word, or null.
+ * @param bigrams a list of bigrams, or null.
+ */
+ public void add(final String word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams) {
+ if (null != shortcutTargets) {
+ addNeutralWords(shortcutTargets);
+ }
+ if (null != bigrams) {
+ addNeutralWords(bigrams);
+ }
+ add(getCodePoints(word), frequency, shortcutTargets, bigrams, false /* isShortcutOnly */);
+ }
+
+ /**
+ * Sanity check for a node.
+ *
+ * This method checks that all CharGroups in a node are ordered as expected.
+ * If they are, nothing happens. If they aren't, an exception is thrown.
+ */
+ private void checkStack(Node node) {
+ ArrayList<CharGroup> stack = node.mData;
+ int lastValue = -1;
+ for (int i = 0; i < stack.size(); ++i) {
+ int currentValue = stack.get(i).mChars[0];
+ if (currentValue <= lastValue)
+ throw new RuntimeException("Invalid stack");
+ else
+ lastValue = currentValue;
+ }
+ }
+
+ /**
+ * Helper method to add a shortcut that should not be a dictionary word.
+ *
+ * @param word the word to add.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param shortcutTargets a list of shortcut targets. May not be null.
+ */
+ public void addShortcutOnly(final String word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets) {
+ if (null == shortcutTargets) {
+ throw new RuntimeException("Can't add a shortcut without targets");
+ }
+ addNeutralWords(shortcutTargets);
+ add(getCodePoints(word), frequency, shortcutTargets, null, true /* isShortcutOnly */);
+ }
+
+ /**
+ * Add a word to this dictionary.
+ *
+ * The shortcuts and bigrams, if any, have to be in the dictionary already. If they aren't,
+ * an exception is thrown.
+ *
+ * @param word the word, as an int array.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param shortcutTargets an optional list of shortcut targets for this word (null if none).
+ * @param bigrams an optional list of bigrams for this word (null if none).
+ * @param isShortcutOnly whether this should be a shortcut only.
+ */
+ private void add(final int[] word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams,
+ final boolean isShortcutOnly) {
+ assert(frequency >= 0 && frequency <= 255);
+ Node currentNode = mRoot;
+ int charIndex = 0;
+
+ CharGroup currentGroup = null;
+ int differentCharIndex = 0; // Set by the loop to the index of the char that differs
+ int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
+ while (CHARACTER_NOT_FOUND != nodeIndex) {
+ currentGroup = currentNode.mData.get(nodeIndex);
+ differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex);
+ if (ARRAYS_ARE_EQUAL != differentCharIndex
+ && differentCharIndex < currentGroup.mChars.length) break;
+ if (null == currentGroup.mChildren) break;
+ charIndex += currentGroup.mChars.length;
+ if (charIndex >= word.length) break;
+ currentNode = currentGroup.mChildren;
+ nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+ }
+
+ if (-1 == nodeIndex) {
+ // No node at this point to accept the word. Create one.
+ final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
+ final CharGroup newGroup = new CharGroup(
+ Arrays.copyOfRange(word, charIndex, word.length),
+ shortcutTargets, bigrams, frequency, isShortcutOnly);
+ currentNode.mData.add(insertionIndex, newGroup);
+ checkStack(currentNode);
+ } else {
+ // There is a word with a common prefix.
+ 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);
+ }
+ } 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.
+ final CharGroup newNode = new CharGroup(
+ Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
+ shortcutTargets, bigrams, frequency, isShortcutOnly);
+ currentGroup.mChildren = new Node();
+ currentGroup.mChildren.mData.add(newNode);
+ }
+ } 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);
+ }
+ } 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.
+ Node newChildren = new Node();
+ final CharGroup newOldWord = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
+ currentGroup.mChars.length), currentGroup.mShortcutTargets,
+ currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren,
+ currentGroup.mIsShortcutOnly);
+ newChildren.mData.add(newOldWord);
+
+ final CharGroup newParent;
+ if (charIndex + differentCharIndex >= word.length) {
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ shortcutTargets, bigrams, frequency, newChildren, isShortcutOnly);
+ } else {
+ // isShortcutOnly makes no sense for non-terminal nodes. The following node
+ // is non-terminal (frequency 0 in FusionDictionary representation) so we
+ // pass false for isShortcutOnly
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ null, null, -1, newChildren, false /* isShortcutOnly */);
+ final CharGroup newWord = new CharGroup(
+ Arrays.copyOfRange(word, charIndex + differentCharIndex,
+ word.length), shortcutTargets, bigrams, frequency,
+ isShortcutOnly);
+ final int addIndex = word[charIndex + differentCharIndex]
+ > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+ newChildren.mData.add(addIndex, newWord);
+ }
+ currentNode.mData.set(nodeIndex, newParent);
+ }
+ checkStack(currentNode);
+ }
+ }
+ }
+
+ /**
+ * Custom comparison of two int arrays taken to contain character codes.
+ *
+ * This method compares the two arrays passed as an argument in a lexicographic way,
+ * with an offset in the dst string.
+ * This method does NOT test for the first character. It is taken to be equal.
+ * I repeat: this method starts the comparison at 1 <> dstOffset + 1.
+ * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the
+ * strings are equal. This works BECAUSE we don't look at the first character.
+ *
+ * @param src the left-hand side string of the comparison.
+ * @param dst the right-hand side string of the comparison.
+ * @param dstOffset the offset in the right-hand side string.
+ * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't.
+ */
+ private static int ARRAYS_ARE_EQUAL = 0;
+ private static int compareArrays(final int[] src, final int[] dst, int dstOffset) {
+ // We do NOT test the first char, because we come from a method that already
+ // tested it.
+ for (int i = 1; i < src.length; ++i) {
+ if (dstOffset + i >= dst.length) return i;
+ if (src[i] != dst[dstOffset + i]) return i;
+ }
+ if (dst.length > src.length) return src.length;
+ return ARRAYS_ARE_EQUAL;
+ }
+
+ /**
+ * Helper class that compares and sorts two chargroups according to their
+ * first element only. I repeat: ONLY the first element is considered, the rest
+ * is ignored.
+ * This comparator imposes orderings that are inconsistent with equals.
+ */
+ static private class CharGroupComparator implements java.util.Comparator {
+ public int compare(Object o1, Object o2) {
+ final CharGroup c1 = (CharGroup)o1;
+ final CharGroup c2 = (CharGroup)o2;
+ if (c1.mChars[0] == c2.mChars[0]) return 0;
+ return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+ }
+ public boolean equals(Object o) {
+ return o instanceof CharGroupComparator;
+ }
+ }
+ final static private CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+
+ /**
+ * Finds the insertion index of a character within a node.
+ */
+ private static int findInsertionIndex(final Node node, int character) {
+ final List data = node.mData;
+ final CharGroup reference = new CharGroup(new int[] { character }, null, null, 0,
+ false /* isShortcutOnly */);
+ int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
+ return result >= 0 ? result : -result - 1;
+ }
+
+ /**
+ * Find the index of a char in a node, if it exists.
+ *
+ * @param node the node to search in.
+ * @param character the character to search for.
+ * @return the position of the character if it's there, or CHARACTER_NOT_FOUND = -1 else.
+ */
+ private static int CHARACTER_NOT_FOUND = -1;
+ private static int findIndexOfChar(final Node node, int character) {
+ final int insertionIndex = findInsertionIndex(node, character);
+ if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND;
+ return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
+ : CHARACTER_NOT_FOUND;
+ }
+
+ /**
+ * Helper method to find a word in a given branch.
+ */
+ public static CharGroup findWordInTree(Node node, final String s) {
+ int index = 0;
+ final StringBuilder checker = new StringBuilder();
+
+ CharGroup currentGroup;
+ do {
+ int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
+ if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
+ currentGroup = node.mData.get(indexOfGroup);
+ checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
+ index += currentGroup.mChars.length;
+ if (index < s.length()) {
+ node = currentGroup.mChildren;
+ }
+ } while (null != node && index < s.length());
+
+ if (!s.equals(checker.toString())) return null;
+ return currentGroup;
+ }
+
+ /**
+ * Helper method to find out whether a word is in the dict or not.
+ */
+ public boolean hasWord(final String s) {
+ if (null == s || "".equals(s)) {
+ throw new RuntimeException("Can't search for a null or empty string");
+ }
+ return null != findWordInTree(mRoot, s);
+ }
+
+ /**
+ * Recursively count the number of character groups in a given branch of the trie.
+ *
+ * @param node the parent node.
+ * @return the number of char groups in all the branch under this node.
+ */
+ public static int countCharGroups(final Node node) {
+ final int nodeSize = node.mData.size();
+ int size = nodeSize;
+ for (int i = nodeSize - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countCharGroups(group.mChildren);
+ }
+ return size;
+ }
+
+ /**
+ * Recursively count the number of nodes in a given branch of the trie.
+ *
+ * @param node the node to count.
+ * @result the number of nodes in this branch.
+ */
+ public static int countNodes(final Node node) {
+ int size = 1;
+ for (int i = node.mData.size() - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countNodes(group.mChildren);
+ }
+ return size;
+ }
+
+ // Historically, the tails of the words were going to be merged to save space.
+ // However, that would prevent the code to search for a specific address in log(n)
+ // time so this was abandoned.
+ // The code is still of interest as it does add some compression to any dictionary
+ // that has no need for attributes. Implementations that does not read attributes should be
+ // able to read a dictionary with merged tails.
+ // Also, the following code does support frequencies, as in, it will only merges
+ // tails that share the same frequency. Though it would result in the above loss of
+ // performance while searching by address, it is still technically possible to merge
+ // tails that contain attributes, but this code does not take that into account - it does
+ // not compare attributes and will merge terminals with different attributes regardless.
+ public void mergeTails() {
+ MakedictLog.i("Do not merge tails");
+ return;
+
+// MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
+// MakedictLog.i("Number of groups : " + countCharGroups(root));
+//
+// final HashMap<String, ArrayList<Node>> repository =
+// new HashMap<String, ArrayList<Node>>();
+// mergeTailsInner(repository, root);
+//
+// MakedictLog.i("Number of different pseudohashes : " + repository.size());
+// int size = 0;
+// for (ArrayList<Node> a : repository.values()) {
+// size += a.size();
+// }
+// MakedictLog.i("Number of nodes after merge : " + (1 + size));
+// MakedictLog.i("Recursively seen nodes : " + countNodes(root));
+ }
+
+ // The following methods are used by the deactivated mergeTails()
+// private static boolean isEqual(Node a, Node b) {
+// if (null == a && null == b) return true;
+// if (null == a || null == b) return false;
+// if (a.data.size() != b.data.size()) return false;
+// final int size = a.data.size();
+// for (int i = size - 1; i >= 0; --i) {
+// CharGroup aGroup = a.data.get(i);
+// CharGroup bGroup = b.data.get(i);
+// if (aGroup.frequency != bGroup.frequency) return false;
+// if (aGroup.alternates == null && bGroup.alternates != null) return false;
+// if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
+// if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
+// if (!isEqual(aGroup.children, bGroup.children)) return false;
+// }
+// return true;
+// }
+
+// static private HashMap<String, ArrayList<Node>> mergeTailsInner(
+// final HashMap<String, ArrayList<Node>> map, final Node node) {
+// final ArrayList<CharGroup> branches = node.data;
+// final int nodeSize = branches.size();
+// for (int i = 0; i < nodeSize; ++i) {
+// CharGroup group = branches.get(i);
+// if (null != group.children) {
+// String pseudoHash = getPseudoHash(group.children);
+// ArrayList<Node> similarList = map.get(pseudoHash);
+// if (null == similarList) {
+// similarList = new ArrayList<Node>();
+// map.put(pseudoHash, similarList);
+// }
+// boolean merged = false;
+// for (Node similar : similarList) {
+// if (isEqual(group.children, similar)) {
+// group.children = similar;
+// merged = true;
+// break;
+// }
+// }
+// if (!merged) {
+// similarList.add(group.children);
+// }
+// mergeTailsInner(map, group.children);
+// }
+// }
+// return map;
+// }
+
+// private static String getPseudoHash(final Node node) {
+// StringBuilder s = new StringBuilder();
+// for (CharGroup g : node.data) {
+// s.append(g.frequency);
+// for (int ch : g.chars){
+// s.append(Character.toChars(ch));
+// }
+// }
+// return s.toString();
+// }
+
+ /**
+ * Iterator to walk through a dictionary.
+ *
+ * This is purely for convenience.
+ */
+ public static class DictionaryIterator implements Iterator<Word> {
+
+ private static class Position {
+ public Iterator<CharGroup> pos;
+ public int length;
+ public Position(ArrayList<CharGroup> groups) {
+ pos = groups.iterator();
+ length = 0;
+ }
+ }
+ final StringBuilder mCurrentString;
+ final LinkedList<Position> mPositions;
+
+ public DictionaryIterator(ArrayList<CharGroup> root) {
+ mCurrentString = new StringBuilder();
+ mPositions = new LinkedList<Position>();
+ final Position rootPos = new Position(root);
+ mPositions.add(rootPos);
+ }
+
+ @Override
+ public boolean hasNext() {
+ for (Position p : mPositions) {
+ if (p.pos.hasNext()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Word next() {
+ Position currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - currentPos.length);
+
+ do {
+ if (currentPos.pos.hasNext()) {
+ final CharGroup currentGroup = currentPos.pos.next();
+ currentPos.length = currentGroup.mChars.length;
+ for (int i : currentGroup.mChars)
+ mCurrentString.append(Character.toChars(i));
+ if (null != currentGroup.mChildren) {
+ currentPos = new Position(currentGroup.mChildren.mData);
+ mPositions.addLast(currentPos);
+ }
+ if (currentGroup.mFrequency >= 0)
+ return new Word(mCurrentString.toString(), currentGroup.mFrequency,
+ currentGroup.mShortcutTargets, currentGroup.mBigrams,
+ currentGroup.mIsShortcutOnly);
+ } else {
+ mPositions.removeLast();
+ currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
+ }
+ } while(true);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Unsupported yet");
+ }
+
+ }
+
+ /**
+ * Method to return an iterator.
+ *
+ * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x
+ * and say : for (Word w : x) {}
+ */
+ @Override
+ public Iterator<Word> iterator() {
+ return new DictionaryIterator(mRoot.mData);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
new file mode 100644
index 000000000..cff8d6fd0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+/**
+ * Wrapper to redirect log events to the right output medium.
+ */
+public class MakedictLog {
+
+ private static void print(String message) {
+ System.out.println(message);
+ }
+
+ public static void d(String message) {
+ print(message);
+ }
+ public static void e(String message) {
+ print(message);
+ }
+ public static void i(String message) {
+ print(message);
+ }
+ public static void w(String message) {
+ print(message);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
new file mode 100644
index 000000000..5b41d27f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+/**
+ * A not-yet-resolved attribute.
+ *
+ * An attribute is either a bigram or a shortcut.
+ * All instances of this class are always immutable.
+ */
+public class PendingAttribute {
+ public final int mFrequency;
+ public final int mAddress;
+ public PendingAttribute(final int frequency, final int address) {
+ mFrequency = frequency;
+ mAddress = address;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Dummy.java b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
index 27ea5ace6..bd42fb8fa 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Dummy.java
+++ b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2012 The Android Open Source Project
+ * Copyright (C) 2011 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
@@ -16,6 +16,11 @@
package com.android.inputmethod.latin.makedict;
-public class Dummy {
-
+/**
+ * Simple exception thrown when a file format is not recognized.
+ */
+public class UnsupportedFormatException extends Exception {
+ public UnsupportedFormatException(String description) {
+ super(description);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java
new file mode 100644
index 000000000..c2c01e1f8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Word.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2011 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.makedict;
+
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class for a word with a frequency.
+ *
+ * This is chiefly used to iterate a dictionary.
+ */
+public class Word implements Comparable<Word> {
+ final String mWord;
+ final int mFrequency;
+ final boolean mIsShortcutOnly;
+ final ArrayList<WeightedString> mShortcutTargets;
+ final ArrayList<WeightedString> mBigrams;
+
+ public Word(final String word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams, final boolean isShortcutOnly) {
+ mWord = word;
+ mFrequency = frequency;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ mIsShortcutOnly = isShortcutOnly;
+ }
+
+ /**
+ * Three-way comparison.
+ *
+ * A Word x is greater than a word y if x has a higher frequency. If they have the same
+ * frequency, they are sorted in lexicographic order.
+ */
+ @Override
+ public int compareTo(Word w) {
+ if (mFrequency < w.mFrequency) return 1;
+ if (mFrequency > w.mFrequency) return -1;
+ return mWord.compareTo(w.mWord);
+ }
+
+ /**
+ * Equality test.
+ *
+ * Words are equal if they have the same frequency, the same spellings, and the same
+ * attributes.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Word)) return false;
+ Word w = (Word)o;
+ return mFrequency == w.mFrequency && mWord.equals(w.mWord)
+ && mShortcutTargets.equals(w.mShortcutTargets)
+ && mBigrams.equals(w.mBigrams);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index 3a17f1f64..dd83a0c4e 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -23,7 +23,6 @@ import android.graphics.drawable.Drawable;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
@@ -53,10 +52,8 @@ public class MoreSuggestions extends Keyboard {
public int mDividerWidth;
public int layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth,
- int maxRow, KeyboardView view) {
+ int maxRow, MoreSuggestionsView view) {
clearKeys();
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
final Resources res = view.getContext().getResources();
mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
// TODO: Drawable itself should have an alpha value.
@@ -64,6 +61,7 @@ public class MoreSuggestions extends Keyboard {
mDividerWidth = mDivider.getIntrinsicWidth();
final int padding = (int) res.getDimension(
R.dimen.more_suggestions_key_horizontal_padding);
+ final Paint paint = view.newDefaultLabelPaint();
int row = 0;
int pos = fromPos, rowStartPos = fromPos;
@@ -71,7 +69,7 @@ public class MoreSuggestions extends Keyboard {
while (pos < size) {
final String word = suggestions.getWord(pos).toString();
// TODO: Should take care of text x-scaling.
- mWidths[pos] = (int)view.getDefaultLabelWidth(word, paint) + padding;
+ mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
final int numColumn = pos - rowStartPos + 1;
final int columnWidth =
(maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index f11b1a4c1..e64e7a685 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -43,17 +43,17 @@ import com.android.inputmethod.latin.R;
public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
private final int[] mCoordinates = new int[2];
- private final KeyDetector mModalPanelKeyDetector;
+ final KeyDetector mModalPanelKeyDetector;
private final KeyDetector mSlidingPanelKeyDetector;
private Controller mController;
- private KeyboardActionListener mListener;
+ KeyboardActionListener mListener;
private int mOriginX;
private int mOriginY;
- private static final TimerProxy EMPTY_TIMER_PROXY = new TimerProxy.Adapter();
+ static final TimerProxy EMPTY_TIMER_PROXY = new TimerProxy.Adapter();
- private final KeyboardActionListener mSuggestionsPaneListener =
+ final KeyboardActionListener mSuggestionsPaneListener =
new KeyboardActionListener.Adapter() {
@Override
public void onPressKey(int primaryCode) {