1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin.spellcheck; 18 19 import android.content.res.Resources; 20 import android.view.textservice.SentenceSuggestionsInfo; 21 import android.view.textservice.SuggestionsInfo; 22 import android.view.textservice.TextInfo; 23 24 import com.android.inputmethod.compat.TextInfoCompatUtils; 25 import com.android.inputmethod.latin.Constants; 26 import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 27 import com.android.inputmethod.latin.utils.RunInLocale; 28 29 import java.util.ArrayList; 30 import java.util.Locale; 31 32 /** 33 * This code is mostly lifted directly from android.service.textservice.SpellCheckerService in 34 * the framework; maybe that should be protected instead, so that implementers don't have to 35 * rewrite everything for any small change. 36 */ 37 public class SentenceLevelAdapter { 38 private static class EmptySentenceSuggestionsInfosInitializationHolder { 39 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 40 new SentenceSuggestionsInfo[]{}; 41 } 42 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 43 getEmptySentenceSuggestionsInfo()44 public static SentenceSuggestionsInfo[] getEmptySentenceSuggestionsInfo() { 45 return EmptySentenceSuggestionsInfosInitializationHolder.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 46 } 47 48 /** 49 * Container for split TextInfo parameters 50 */ 51 public static class SentenceWordItem { 52 public final TextInfo mTextInfo; 53 public final int mStart; 54 public final int mLength; SentenceWordItem(TextInfo ti, int start, int end)55 public SentenceWordItem(TextInfo ti, int start, int end) { 56 mTextInfo = ti; 57 mStart = start; 58 mLength = end - start; 59 } 60 } 61 62 /** 63 * Container for originally queried TextInfo and parameters 64 */ 65 public static class SentenceTextInfoParams { 66 final TextInfo mOriginalTextInfo; 67 final ArrayList<SentenceWordItem> mItems; 68 final int mSize; SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items)69 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 70 mOriginalTextInfo = ti; 71 mItems = items; 72 mSize = items.size(); 73 } 74 } 75 76 private static class WordIterator { 77 private final SpacingAndPunctuations mSpacingAndPunctuations; WordIterator(final Resources res, final Locale locale)78 public WordIterator(final Resources res, final Locale locale) { 79 final RunInLocale<SpacingAndPunctuations> job 80 = new RunInLocale<SpacingAndPunctuations>() { 81 @Override 82 protected SpacingAndPunctuations job(final Resources res) { 83 return new SpacingAndPunctuations(res); 84 } 85 }; 86 mSpacingAndPunctuations = job.runInLocale(res, locale); 87 } 88 getEndOfWord(final CharSequence sequence, int index)89 public int getEndOfWord(final CharSequence sequence, int index) { 90 final int length = sequence.length(); 91 index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); 92 while (index < length) { 93 final int codePoint = Character.codePointAt(sequence, index); 94 if (mSpacingAndPunctuations.isWordSeparator(codePoint)) { 95 // If it's a period, we want to stop here only if it's followed by another 96 // word separator. In all other cases we stop here. 97 if (Constants.CODE_PERIOD == codePoint) { 98 final int indexOfNextCodePoint = 99 index + Character.charCount(Constants.CODE_PERIOD); 100 if (indexOfNextCodePoint < length 101 && mSpacingAndPunctuations.isWordSeparator( 102 Character.codePointAt(sequence, indexOfNextCodePoint))) { 103 return index; 104 } 105 } else { 106 return index; 107 } 108 } 109 index += Character.charCount(codePoint); 110 } 111 return index; 112 } 113 114 public int getBeginningOfNextWord(final CharSequence sequence, int index) { 115 final int length = sequence.length(); 116 if (index >= length) { 117 return -1; 118 } 119 index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1); 120 while (index < length) { 121 final int codePoint = Character.codePointAt(sequence, index); 122 if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) { 123 return index; 124 } 125 index += Character.charCount(codePoint); 126 } 127 return -1; 128 } 129 } 130 131 private final WordIterator mWordIterator; 132 public SentenceLevelAdapter(final Resources res, final Locale locale) { 133 mWordIterator = new WordIterator(res, locale); 134 } 135 136 public SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 137 final WordIterator wordIterator = mWordIterator; 138 final CharSequence originalText = 139 TextInfoCompatUtils.getCharSequenceOrString(originalTextInfo); 140 final int cookie = originalTextInfo.getCookie(); 141 final int start = -1; 142 final int end = originalText.length(); 143 final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); 144 int wordStart = wordIterator.getBeginningOfNextWord(originalText, start); 145 int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); 146 while (wordStart <= end && wordEnd != -1 && wordStart != -1) { 147 if (wordEnd >= start && wordEnd > wordStart) { 148 CharSequence subSequence = originalText.subSequence(wordStart, wordEnd).toString(); 149 final TextInfo ti = TextInfoCompatUtils.newInstance(subSequence, 0, 150 subSequence.length(), cookie, subSequence.hashCode()); 151 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 152 } 153 wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd); 154 if (wordStart == -1) { 155 break; 156 } 157 wordEnd = wordIterator.getEndOfWord(originalText, wordStart); 158 } 159 return new SentenceTextInfoParams(originalTextInfo, wordItems); 160 } 161 162 public static SentenceSuggestionsInfo reconstructSuggestions( 163 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 164 if (results == null || results.length == 0) { 165 return null; 166 } 167 if (originalTextInfoParams == null) { 168 return null; 169 } 170 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 171 final int originalSequence = 172 originalTextInfoParams.mOriginalTextInfo.getSequence(); 173 174 final int querySize = originalTextInfoParams.mSize; 175 final int[] offsets = new int[querySize]; 176 final int[] lengths = new int[querySize]; 177 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 178 for (int i = 0; i < querySize; ++i) { 179 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 180 SuggestionsInfo result = null; 181 for (int j = 0; j < results.length; ++j) { 182 final SuggestionsInfo cur = results[j]; 183 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 184 result = cur; 185 result.setCookieAndSequence(originalCookie, originalSequence); 186 break; 187 } 188 } 189 offsets[i] = item.mStart; 190 lengths[i] = item.mLength; 191 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 192 } 193 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 194 } 195 } 196