459b4f353e
We want to let the facilitator decide if a word is valid or invalid, and cache the answer in the facilitator's cache. The spell checker session doesn't need its own word cache, except as a crutch to communicate suggestions to the code that populates the suggestion drop-down. We leave that in place. Bug 20018546. Change-Id: I3c3c53e0c1d709fa2f64a2952a232acd7380b57a
225 lines
10 KiB
Java
225 lines
10 KiB
Java
/*
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.inputmethod.latin.spellcheck;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.content.res.Resources;
|
|
import android.os.Binder;
|
|
import android.os.Build;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
import android.view.textservice.SentenceSuggestionsInfo;
|
|
import android.view.textservice.SuggestionsInfo;
|
|
import android.view.textservice.TextInfo;
|
|
|
|
import com.android.inputmethod.compat.TextInfoCompatUtils;
|
|
import com.android.inputmethod.latin.NgramContext;
|
|
import com.android.inputmethod.latin.utils.SpannableStringUtils;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Locale;
|
|
|
|
public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession {
|
|
private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName();
|
|
private static final boolean DBG = false;
|
|
private final Resources mResources;
|
|
private SentenceLevelAdapter mSentenceLevelAdapter;
|
|
|
|
public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
|
|
super(service);
|
|
mResources = service.getResources();
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
|
private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
|
|
SentenceSuggestionsInfo ssi) {
|
|
final CharSequence typedText = TextInfoCompatUtils.getCharSequenceOrString(ti);
|
|
if (!typedText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
|
|
return null;
|
|
}
|
|
final int N = ssi.getSuggestionsCount();
|
|
final ArrayList<Integer> additionalOffsets = new ArrayList<>();
|
|
final ArrayList<Integer> additionalLengths = new ArrayList<>();
|
|
final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = new ArrayList<>();
|
|
CharSequence currentWord = null;
|
|
for (int i = 0; i < N; ++i) {
|
|
final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
|
|
final int flags = si.getSuggestionsAttributes();
|
|
if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
|
|
continue;
|
|
}
|
|
final int offset = ssi.getOffsetAt(i);
|
|
final int length = ssi.getLengthAt(i);
|
|
final CharSequence subText = typedText.subSequence(offset, offset + length);
|
|
final NgramContext ngramContext =
|
|
new NgramContext(new NgramContext.WordInfo(currentWord));
|
|
currentWord = subText;
|
|
if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
|
|
continue;
|
|
}
|
|
// Split preserving spans.
|
|
final CharSequence[] splitTexts = SpannableStringUtils.split(subText,
|
|
AndroidSpellCheckerService.SINGLE_QUOTE,
|
|
true /* preserveTrailingEmptySegments */);
|
|
if (splitTexts == null || splitTexts.length <= 1) {
|
|
continue;
|
|
}
|
|
final int splitNum = splitTexts.length;
|
|
for (int j = 0; j < splitNum; ++j) {
|
|
final CharSequence splitText = splitTexts[j];
|
|
if (TextUtils.isEmpty(splitText)) {
|
|
continue;
|
|
}
|
|
if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == null) {
|
|
continue;
|
|
}
|
|
final int newLength = splitText.length();
|
|
// Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
|
|
final int newFlags = 0;
|
|
final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
|
|
newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
|
|
if (DBG) {
|
|
Log.d(TAG, "Override and remove old span over: " + splitText + ", "
|
|
+ offset + "," + newLength);
|
|
}
|
|
additionalOffsets.add(offset);
|
|
additionalLengths.add(newLength);
|
|
additionalSuggestionsInfos.add(newSi);
|
|
}
|
|
}
|
|
final int additionalSize = additionalOffsets.size();
|
|
if (additionalSize <= 0) {
|
|
return null;
|
|
}
|
|
final int suggestionsSize = N + additionalSize;
|
|
final int[] newOffsets = new int[suggestionsSize];
|
|
final int[] newLengths = new int[suggestionsSize];
|
|
final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
|
|
int i;
|
|
for (i = 0; i < N; ++i) {
|
|
newOffsets[i] = ssi.getOffsetAt(i);
|
|
newLengths[i] = ssi.getLengthAt(i);
|
|
newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
|
|
}
|
|
for (; i < suggestionsSize; ++i) {
|
|
newOffsets[i] = additionalOffsets.get(i - N);
|
|
newLengths[i] = additionalLengths.get(i - N);
|
|
newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
|
|
}
|
|
return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
|
|
}
|
|
|
|
@Override
|
|
public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
|
|
int suggestionsLimit) {
|
|
final SentenceSuggestionsInfo[] retval = splitAndSuggest(textInfos, suggestionsLimit);
|
|
if (retval == null || retval.length != textInfos.length) {
|
|
return retval;
|
|
}
|
|
for (int i = 0; i < retval.length; ++i) {
|
|
final SentenceSuggestionsInfo tempSsi =
|
|
fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
|
|
if (tempSsi != null) {
|
|
retval[i] = tempSsi;
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
* Get sentence suggestions for specified texts in an array of TextInfo. This is taken from
|
|
* SpellCheckerService#onGetSentenceSuggestionsMultiple that we can't use because it's
|
|
* using private variables.
|
|
* The default implementation splits the input text to words and returns
|
|
* {@link SentenceSuggestionsInfo} which contains suggestions for each word.
|
|
* This function will run on the incoming IPC thread.
|
|
* So, this is not called on the main thread,
|
|
* but will be called in series on another thread.
|
|
* @param textInfos an array of the text metadata
|
|
* @param suggestionsLimit the maximum number of suggestions to be returned
|
|
* @return an array of {@link SentenceSuggestionsInfo} returned by
|
|
* {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
|
|
*/
|
|
private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) {
|
|
if (textInfos == null || textInfos.length == 0) {
|
|
return SentenceLevelAdapter.getEmptySentenceSuggestionsInfo();
|
|
}
|
|
SentenceLevelAdapter sentenceLevelAdapter;
|
|
synchronized(this) {
|
|
sentenceLevelAdapter = mSentenceLevelAdapter;
|
|
if (sentenceLevelAdapter == null) {
|
|
final String localeStr = getLocale();
|
|
if (!TextUtils.isEmpty(localeStr)) {
|
|
sentenceLevelAdapter = new SentenceLevelAdapter(mResources,
|
|
new Locale(localeStr));
|
|
mSentenceLevelAdapter = sentenceLevelAdapter;
|
|
}
|
|
}
|
|
}
|
|
if (sentenceLevelAdapter == null) {
|
|
return SentenceLevelAdapter.getEmptySentenceSuggestionsInfo();
|
|
}
|
|
final int infosSize = textInfos.length;
|
|
final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize];
|
|
for (int i = 0; i < infosSize; ++i) {
|
|
final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams =
|
|
sentenceLevelAdapter.getSplitWords(textInfos[i]);
|
|
final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems =
|
|
textInfoParams.mItems;
|
|
final int itemsSize = mItems.size();
|
|
final TextInfo[] splitTextInfos = new TextInfo[itemsSize];
|
|
for (int j = 0; j < itemsSize; ++j) {
|
|
splitTextInfos[j] = mItems.get(j).mTextInfo;
|
|
}
|
|
retval[i] = SentenceLevelAdapter.reconstructSuggestions(
|
|
textInfoParams, onGetSuggestionsMultiple(
|
|
splitTextInfos, suggestionsLimit, true));
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
@Override
|
|
public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
|
|
int suggestionsLimit, boolean sequentialWords) {
|
|
long ident = Binder.clearCallingIdentity();
|
|
try {
|
|
final int length = textInfos.length;
|
|
final SuggestionsInfo[] retval = new SuggestionsInfo[length];
|
|
for (int i = 0; i < length; ++i) {
|
|
final CharSequence prevWord;
|
|
if (sequentialWords && i > 0) {
|
|
final TextInfo prevTextInfo = textInfos[i - 1];
|
|
final CharSequence prevWordCandidate =
|
|
TextInfoCompatUtils.getCharSequenceOrString(prevTextInfo);
|
|
// Note that an empty string would be used to indicate the initial word
|
|
// in the future.
|
|
prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
|
|
} else {
|
|
prevWord = null;
|
|
}
|
|
final NgramContext ngramContext =
|
|
new NgramContext(new NgramContext.WordInfo(prevWord));
|
|
final TextInfo textInfo = textInfos[i];
|
|
retval[i] = onGetSuggestionsInternal(textInfo, ngramContext, suggestionsLimit);
|
|
retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence());
|
|
}
|
|
return retval;
|
|
} finally {
|
|
Binder.restoreCallingIdentity(ident);
|
|
}
|
|
}
|
|
}
|