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.annotation.TargetApi; 20 import android.content.res.Resources; 21 import android.os.Build; 22 import android.view.textservice.SentenceSuggestionsInfo; 23 import android.view.textservice.SuggestionsInfo; 24 import android.view.textservice.TextInfo; 25 26 import com.android.inputmethod.compat.TextInfoCompatUtils; 27 import com.android.inputmethod.latin.common.Constants; 28 import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 29 import com.android.inputmethod.latin.utils.RunInLocale; 30 31 import java.util.ArrayList; 32 import java.util.Locale; 33 34 /** 35 * This code is mostly lifted directly from android.service.textservice.SpellCheckerService in 36 * the framework; maybe that should be protected instead, so that implementers don't have to 37 * rewrite everything for any small change. 38 */ 39 public class SentenceLevelAdapter { 40 private static class EmptySentenceSuggestionsInfosInitializationHolder { 41 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 42 new SentenceSuggestionsInfo[]{}; 43 } 44 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 45 46 public static SentenceSuggestionsInfo[] getEmptySentenceSuggestionsInfo() { 47 return EmptySentenceSuggestionsInfosInitializationHolder.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 48 } 49 50 /** 51 * Container for split TextInfo parameters 52 */ 53 public static class SentenceWordItem { 54 public final TextInfo mTextInfo; 55 public final int mStart; 56 public final int mLength; 57 public SentenceWordItem(TextInfo ti, int start, int end) { 58 mTextInfo = ti; 59 mStart = start; 60 mLength = end - start; 61 } 62 } 63 64 /** 65 * Container for originally queried TextInfo and parameters 66 */ 67 public static class SentenceTextInfoParams { 68 final TextInfo mOriginalTextInfo; 69 final ArrayList<SentenceWordItem> mItems; 70 final int mSize; 71 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 72 mOriginalTextInfo = ti; 73 mItems = items; 74 mSize = items.size(); 75 } 76 } 77 78 private static class WordIterator { 79 private final SpacingAndPunctuations mSpacingAndPunctuations; 80 public WordIterator(final Resources res, final Locale locale) { 81 final RunInLocale<SpacingAndPunctuations> job = 82 new RunInLocale<SpacingAndPunctuations>() { 83 @Override 84 protected SpacingAndPunctuations job(final Resources r) { 85 return new SpacingAndPunctuations(r); 86 } 87 }; 88 mSpacingAndPunctuations = job.runInLocale(res, locale); 89 } 90 91 public int getEndOfWord(final CharSequence sequence, final int fromIndex) { 92 final int length = sequence.length(); 93 int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 1); 94 while (index < length) { 95 final int codePoint = Character.codePointAt(sequence, index); 96 if (mSpacingAndPunctuations.isWordSeparator(codePoint)) { 97 // If it's a period, we want to stop here only if it's followed by another 98 // word separator. In all other cases we stop here. 99 if (Constants.CODE_PERIOD == codePoint) { 100 final int indexOfNextCodePoint = 101 index + Character.charCount(Constants.CODE_PERIOD); 102 if (indexOfNextCodePoint < length 103 && mSpacingAndPunctuations.isWordSeparator( 104 Character.codePointAt(sequence, indexOfNextCodePoint))) { 105 return index; 106 } 107 } else { 108 return index; 109 } 110 } 111 index += Character.charCount(codePoint); 112 } 113 return index; 114 } 115 116 public int getBeginningOfNextWord(final CharSequence sequence, final int fromIndex) { 117 final int length = sequence.length(); 118 if (fromIndex >= length) { 119 return -1; 120 } 121 int index = fromIndex < 0 ? 0 : Character.offsetByCodePoints(sequence, fromIndex, 1); 122 while (index < length) { 123 final int codePoint = Character.codePointAt(sequence, index); 124 if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) { 125 return index; 126 } 127 index += Character.charCount(codePoint); 128 } 129 return -1; 130 } 131 } 132 133 private final WordIterator mWordIterator; 134 public SentenceLevelAdapter(final Resources res, final Locale locale) { 135 mWordIterator = new WordIterator(res, locale); 136 } 137 138 public SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 139 final WordIterator wordIterator = mWordIterator; 140 final CharSequence originalText = 141 TextInfoCompatUtils.getCharSequenceOrString(originalTextInfo); 142 final int cookie = originalTextInfo.getCookie(); 143 final int start = -1; 144 final int end = originalText.length(); 145 final ArrayList<SentenceWordItem> wordItems = new ArrayList<>(); 146 int wordStart = wordIterator.getBeginningOfNextWord(originalText, start); 147 int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); 148 while (wordStart <= end && wordEnd != -1 && wordStart != -1) { 149 if (wordEnd >= start && wordEnd > wordStart) { 150 final TextInfo ti = TextInfoCompatUtils.newInstance(originalText, wordStart, 151 wordEnd, cookie, originalText.subSequence(wordStart, wordEnd).hashCode()); 152 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 153 } 154 wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd); 155 if (wordStart == -1) { 156 break; 157 } 158 wordEnd = wordIterator.getEndOfWord(originalText, wordStart); 159 } 160 return new SentenceTextInfoParams(originalTextInfo, wordItems); 161 } 162 163 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 164 public static SentenceSuggestionsInfo reconstructSuggestions( 165 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 166 if (results == null || results.length == 0) { 167 return null; 168 } 169 if (originalTextInfoParams == null) { 170 return null; 171 } 172 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 173 final int originalSequence = 174 originalTextInfoParams.mOriginalTextInfo.getSequence(); 175 176 final int querySize = originalTextInfoParams.mSize; 177 final int[] offsets = new int[querySize]; 178 final int[] lengths = new int[querySize]; 179 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 180 for (int i = 0; i < querySize; ++i) { 181 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 182 SuggestionsInfo result = null; 183 for (int j = 0; j < results.length; ++j) { 184 final SuggestionsInfo cur = results[j]; 185 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 186 result = cur; 187 result.setCookieAndSequence(originalCookie, originalSequence); 188 break; 189 } 190 } 191 offsets[i] = item.mStart; 192 lengths[i] = item.mLength; 193 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 194 } 195 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 196 } 197 } 198