/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.latin; import android.text.TextUtils; import android.util.SparseArray; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; /** * Implements a static, compacted, binary dictionary of standard words. */ public final class BinaryDictionary extends Dictionary { private static final String TAG = BinaryDictionary.class.getSimpleName(); // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; // Must be equal to MAX_RESULTS in native/jni/src/defines.h private static final int MAX_RESULTS = 18; private long mNativeDict; private final Locale mLocale; private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS]; private final int[] mSpaceIndices = new int[MAX_RESULTS]; private final int[] mOutputScores = new int[MAX_RESULTS]; private final int[] mOutputTypes = new int[MAX_RESULTS]; private final boolean mUseFullEditDistance; private final SparseArray mDicTraverseSessions = CollectionUtils.newSparseArray(); // TODO: There should be a way to remove used DicTraverseSession objects from // {@code mDicTraverseSessions}. private DicTraverseSession getTraverseSession(final int traverseSessionId) { synchronized(mDicTraverseSessions) { DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId); if (traverseSession == null) { traverseSession = mDicTraverseSessions.get(traverseSessionId); if (traverseSession == null) { traverseSession = new DicTraverseSession(mLocale, mNativeDict); mDicTraverseSessions.put(traverseSessionId, traverseSession); } } return traverseSession; } } /** * Constructor for the binary dictionary. This is supposed to be called from the * dictionary factory. * @param filename the name of the file to read through native code. * @param offset the offset of the dictionary data within the file. * @param length the length of the binary data. * @param useFullEditDistance whether to use the full edit distance in suggestions * @param dictType the dictionary type, as a human-readable string */ public BinaryDictionary(final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType) { super(dictType); mLocale = locale; mUseFullEditDistance = useFullEditDistance; loadDictionary(filename, offset, length); } static { JniUtils.loadNativeLibrary(); } private static native long openNative(String sourceDir, long dictOffset, long dictSize); private static native void closeNative(long dict); private static native int getProbabilityNative(long dict, int[] word); private static native boolean isValidBigramNative(long dict, int[] word1, int[] word2); private static native int getSuggestionsNative(long dict, long proximityInfo, long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, boolean isGesture, int[] prevWordCodePointArray, boolean useFullEditDistance, int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes); private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); private static native int editDistanceNative(int[] before, int[] after); // TODO: Move native dict into session private final void loadDictionary(final String path, final long startOffset, final long length) { mNativeDict = openNative(path, startOffset, length); } @Override public ArrayList getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords) { return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, 0 /* sessionId */); } @Override public ArrayList getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int sessionId) { if (!isValidDictionary()) return null; Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE); // TODO: toLowerCase in the native code final int[] prevWordCodePointArray = (null == prevWord) ? null : StringUtils.toCodePointArray(prevWord); final int composerSize = composer.size(); final boolean isGesture = composer.isBatchMode(); if (composerSize <= 1 || !isGesture) { if (composerSize > MAX_WORD_LENGTH - 1) return null; for (int i = 0; i < composerSize; i++) { mInputCodePoints[i] = composer.getCodeAt(i); } } final InputPointers ips = composer.getInputPointers(); final int inputSize = isGesture ? ips.getPointerSize() : composerSize; // proximityInfo and/or prevWordForBigrams may not be null. final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), getTraverseSession(sessionId).getSession(), ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, inputSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray, mUseFullEditDistance, mOutputCodePoints, mOutputScores, mSpaceIndices, mOutputTypes); final ArrayList suggestions = CollectionUtils.newArrayList(); for (int j = 0; j < count; ++j) { final int start = j * MAX_WORD_LENGTH; int len = 0; while (len < MAX_WORD_LENGTH && mOutputCodePoints[start + len] != 0) { ++len; } if (len > 0) { final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS; if (blockOffensiveWords && 0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE) && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) { // If we block potentially offensive words, and if the word is possibly // offensive, then we don't output it unless it's also an exact match. continue; } final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND; final int score = SuggestedWordInfo.KIND_WHITELIST == kind ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j]; // TODO: check that all users of the `kind' parameter are ready to accept // flags too and pass mOutputTypes[j] instead of kind suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len), score, kind, mDictType)); } } return suggestions; } public boolean isValidDictionary() { return mNativeDict != 0; } public static float calcNormalizedScore(final String before, final String after, final int score) { return calcNormalizedScoreNative(StringUtils.toCodePointArray(before), StringUtils.toCodePointArray(after), score); } public static int editDistance(final String before, final String after) { if (before == null || after == null) { throw new IllegalArgumentException(); } return editDistanceNative(StringUtils.toCodePointArray(before), StringUtils.toCodePointArray(after)); } @Override public boolean isValidWord(final String word) { return getFrequency(word) >= 0; } @Override public int getFrequency(final String word) { if (word == null) return -1; int[] codePoints = StringUtils.toCodePointArray(word); return getProbabilityNative(mNativeDict, codePoints); } // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni // calls when checking for changes in an entire dictionary. public boolean isValidBigram(final String word1, final String word2) { if (TextUtils.isEmpty(word1) || TextUtils.isEmpty(word2)) return false; final int[] codePoints1 = StringUtils.toCodePointArray(word1); final int[] codePoints2 = StringUtils.toCodePointArray(word2); return isValidBigramNative(mNativeDict, codePoints1, codePoints2); } @Override public void close() { synchronized (mDicTraverseSessions) { final int sessionsSize = mDicTraverseSessions.size(); for (int index = 0; index < sessionsSize; ++index) { final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index); if (traverseSession != null) { traverseSession.close(); } } } closeInternal(); } private synchronized void closeInternal() { if (mNativeDict != 0) { closeNative(mNativeDict); mNativeDict = 0; } } @Override protected void finalize() throws Throwable { try { closeInternal(); } finally { super.finalize(); } } }