SpellCheckerService.java revision c7ee1b9369ffd7c21a70738056a82dc4238e7fc1
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package android.service.textservice; 18 19import com.android.internal.textservice.ISpellCheckerService; 20import com.android.internal.textservice.ISpellCheckerSession; 21import com.android.internal.textservice.ISpellCheckerSessionListener; 22 23import android.app.Service; 24import android.content.Intent; 25import android.os.Bundle; 26import android.os.IBinder; 27import android.os.Process; 28import android.os.RemoteException; 29import android.text.TextUtils; 30import android.text.method.WordIterator; 31import android.util.Log; 32import android.view.textservice.SentenceSuggestionsInfo; 33import android.view.textservice.SuggestionsInfo; 34import android.view.textservice.TextInfo; 35import android.widget.SpellChecker; 36 37import java.lang.ref.WeakReference; 38import java.text.BreakIterator; 39import java.util.ArrayList; 40import java.util.Locale; 41 42/** 43 * SpellCheckerService provides an abstract base class for a spell checker. 44 * This class combines a service to the system with the spell checker service interface that 45 * spell checker must implement. 46 * 47 * <p>In addition to the normal Service lifecycle methods, this class 48 * introduces a new specific callback that subclasses should override 49 * {@link #createSession()} to provide a spell checker session that is corresponding 50 * to requested language and so on. The spell checker session returned by this method 51 * should extend {@link SpellCheckerService.Session}. 52 * </p> 53 * 54 * <h3>Returning spell check results</h3> 55 * 56 * <p>{@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 57 * should return spell check results. 58 * It receives {@link android.view.textservice.TextInfo} and returns 59 * {@link android.view.textservice.SuggestionsInfo} for the input. 60 * You may want to override 61 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} for 62 * better performance and quality. 63 * </p> 64 * 65 * <p>Please note that {@link SpellCheckerService.Session#getLocale()} does not return a valid 66 * locale before {@link SpellCheckerService.Session#onCreate()} </p> 67 * 68 */ 69public abstract class SpellCheckerService extends Service { 70 private static final String TAG = SpellCheckerService.class.getSimpleName(); 71 private static final boolean DBG = false; 72 public static final String SERVICE_INTERFACE = 73 "android.service.textservice.SpellCheckerService"; 74 75 private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this); 76 77 78 /** 79 * Implement to return the implementation of the internal spell checker 80 * service interface. Subclasses should not override. 81 */ 82 @Override 83 public final IBinder onBind(final Intent intent) { 84 if (DBG) { 85 Log.w(TAG, "onBind"); 86 } 87 return mBinder; 88 } 89 90 /** 91 * Factory method to create a spell checker session impl 92 * @return SpellCheckerSessionImpl which should be overridden by a concrete implementation. 93 */ 94 public abstract Session createSession(); 95 96 /** 97 * This abstract class should be overridden by a concrete implementation of a spell checker. 98 */ 99 public static abstract class Session { 100 private InternalISpellCheckerSession mInternalSession; 101 private volatile SentenceLevelAdapter mSentenceLevelAdapter; 102 103 /** 104 * @hide 105 */ 106 public final void setInternalISpellCheckerSession(InternalISpellCheckerSession session) { 107 mInternalSession = session; 108 } 109 110 /** 111 * This is called after the class is initialized, at which point it knows it can call 112 * getLocale() etc... 113 */ 114 public abstract void onCreate(); 115 116 /** 117 * Get suggestions for specified text in TextInfo. 118 * This function will run on the incoming IPC thread. 119 * So, this is not called on the main thread, 120 * but will be called in series on another thread. 121 * @param textInfo the text metadata 122 * @param suggestionsLimit the maximum number of suggestions to be returned 123 * @return SuggestionsInfo which contains suggestions for textInfo 124 */ 125 public abstract SuggestionsInfo onGetSuggestions(TextInfo textInfo, int suggestionsLimit); 126 127 /** 128 * A batch process of onGetSuggestions. 129 * This function will run on the incoming IPC thread. 130 * So, this is not called on the main thread, 131 * but will be called in series on another thread. 132 * @param textInfos an array of the text metadata 133 * @param suggestionsLimit the maximum number of suggestions to be returned 134 * @param sequentialWords true if textInfos can be treated as sequential words. 135 * @return an array of {@link SentenceSuggestionsInfo} returned by 136 * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 137 */ 138 public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, 139 int suggestionsLimit, boolean sequentialWords) { 140 final int length = textInfos.length; 141 final SuggestionsInfo[] retval = new SuggestionsInfo[length]; 142 for (int i = 0; i < length; ++i) { 143 retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit); 144 retval[i].setCookieAndSequence( 145 textInfos[i].getCookie(), textInfos[i].getSequence()); 146 } 147 return retval; 148 } 149 150 /** 151 * Get sentence suggestions for specified texts in an array of TextInfo. 152 * The default implementation splits the input text to words and returns 153 * {@link SentenceSuggestionsInfo} which contains suggestions for each word. 154 * This function will run on the incoming IPC thread. 155 * So, this is not called on the main thread, 156 * but will be called in series on another thread. 157 * When you override this method, make sure that suggestionsLimit is applied to suggestions 158 * that share the same start position and length. 159 * @param textInfos an array of the text metadata 160 * @param suggestionsLimit the maximum number of suggestions to be returned 161 * @return an array of {@link SentenceSuggestionsInfo} returned by 162 * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 163 */ 164 public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, 165 int suggestionsLimit) { 166 if (textInfos == null || textInfos.length == 0) { 167 return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 168 } 169 if (DBG) { 170 Log.d(TAG, "onGetSentenceSuggestionsMultiple: + " + textInfos.length + ", " 171 + suggestionsLimit); 172 } 173 if (mSentenceLevelAdapter == null) { 174 synchronized(this) { 175 if (mSentenceLevelAdapter == null) { 176 final String localeStr = getLocale(); 177 if (!TextUtils.isEmpty(localeStr)) { 178 mSentenceLevelAdapter = new SentenceLevelAdapter(new Locale(localeStr)); 179 } 180 } 181 } 182 } 183 if (mSentenceLevelAdapter == null) { 184 return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 185 } 186 final int infosSize = textInfos.length; 187 final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; 188 for (int i = 0; i < infosSize; ++i) { 189 final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = 190 mSentenceLevelAdapter.getSplitWords(textInfos[i]); 191 final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems = 192 textInfoParams.mItems; 193 final int itemsSize = mItems.size(); 194 final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; 195 for (int j = 0; j < itemsSize; ++j) { 196 splitTextInfos[j] = mItems.get(j).mTextInfo; 197 } 198 retval[i] = SentenceLevelAdapter.reconstructSuggestions( 199 textInfoParams, onGetSuggestionsMultiple( 200 splitTextInfos, suggestionsLimit, true)); 201 } 202 return retval; 203 } 204 205 /** 206 * Request to abort all tasks executed in SpellChecker. 207 * This function will run on the incoming IPC thread. 208 * So, this is not called on the main thread, 209 * but will be called in series on another thread. 210 */ 211 public void onCancel() {} 212 213 /** 214 * Request to close this session. 215 * This function will run on the incoming IPC thread. 216 * So, this is not called on the main thread, 217 * but will be called in series on another thread. 218 */ 219 public void onClose() {} 220 221 /** 222 * @return Locale for this session 223 */ 224 public String getLocale() { 225 return mInternalSession.getLocale(); 226 } 227 228 /** 229 * @return Bundle for this session 230 */ 231 public Bundle getBundle() { 232 return mInternalSession.getBundle(); 233 } 234 } 235 236 // Preventing from exposing ISpellCheckerSession.aidl, create an internal class. 237 private static class InternalISpellCheckerSession extends ISpellCheckerSession.Stub { 238 private ISpellCheckerSessionListener mListener; 239 private final Session mSession; 240 private final String mLocale; 241 private final Bundle mBundle; 242 243 public InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, 244 Bundle bundle, Session session) { 245 mListener = listener; 246 mSession = session; 247 mLocale = locale; 248 mBundle = bundle; 249 session.setInternalISpellCheckerSession(this); 250 } 251 252 @Override 253 public void onGetSuggestionsMultiple( 254 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 255 int pri = Process.getThreadPriority(Process.myTid()); 256 try { 257 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 258 mListener.onGetSuggestions( 259 mSession.onGetSuggestionsMultiple( 260 textInfos, suggestionsLimit, sequentialWords)); 261 } catch (RemoteException e) { 262 } finally { 263 Process.setThreadPriority(pri); 264 } 265 } 266 267 @Override 268 public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 269 try { 270 mListener.onGetSentenceSuggestions( 271 mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit)); 272 } catch (RemoteException e) { 273 } 274 } 275 276 @Override 277 public void onCancel() { 278 int pri = Process.getThreadPriority(Process.myTid()); 279 try { 280 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 281 mSession.onCancel(); 282 } finally { 283 Process.setThreadPriority(pri); 284 } 285 } 286 287 @Override 288 public void onClose() { 289 int pri = Process.getThreadPriority(Process.myTid()); 290 try { 291 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 292 mSession.onClose(); 293 } finally { 294 Process.setThreadPriority(pri); 295 mListener = null; 296 } 297 } 298 299 public String getLocale() { 300 return mLocale; 301 } 302 303 public Bundle getBundle() { 304 return mBundle; 305 } 306 } 307 308 private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub { 309 private final WeakReference<SpellCheckerService> mInternalServiceRef; 310 311 public SpellCheckerServiceBinder(SpellCheckerService service) { 312 mInternalServiceRef = new WeakReference<SpellCheckerService>(service); 313 } 314 315 @Override 316 public ISpellCheckerSession getISpellCheckerSession( 317 String locale, ISpellCheckerSessionListener listener, Bundle bundle) { 318 final SpellCheckerService service = mInternalServiceRef.get(); 319 if (service == null) return null; 320 final Session session = service.createSession(); 321 final InternalISpellCheckerSession internalSession = 322 new InternalISpellCheckerSession(locale, listener, bundle, session); 323 session.onCreate(); 324 return internalSession; 325 } 326 } 327 328 /** 329 * Adapter class to accommodate word level spell checking APIs to sentence level spell checking 330 * APIs used in 331 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} 332 */ 333 private static class SentenceLevelAdapter { 334 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 335 new SentenceSuggestionsInfo[] {}; 336 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 337 /** 338 * Container for split TextInfo parameters 339 */ 340 public static class SentenceWordItem { 341 public final TextInfo mTextInfo; 342 public final int mStart; 343 public final int mLength; 344 public SentenceWordItem(TextInfo ti, int start, int end) { 345 mTextInfo = ti; 346 mStart = start; 347 mLength = end - start; 348 } 349 } 350 351 /** 352 * Container for originally queried TextInfo and parameters 353 */ 354 public static class SentenceTextInfoParams { 355 final TextInfo mOriginalTextInfo; 356 final ArrayList<SentenceWordItem> mItems; 357 final int mSize; 358 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 359 mOriginalTextInfo = ti; 360 mItems = items; 361 mSize = items.size(); 362 } 363 } 364 365 private final WordIterator mWordIterator; 366 public SentenceLevelAdapter(Locale locale) { 367 mWordIterator = new WordIterator(locale); 368 } 369 370 private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 371 final WordIterator wordIterator = mWordIterator; 372 final CharSequence originalText = originalTextInfo.getText(); 373 final int cookie = originalTextInfo.getCookie(); 374 final int start = 0; 375 final int end = originalText.length(); 376 final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); 377 wordIterator.setCharSequence(originalText, 0, originalText.length()); 378 int wordEnd = wordIterator.following(start); 379 int wordStart = wordIterator.getBeginning(wordEnd); 380 if (DBG) { 381 Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " 382 + wordEnd + "\n" + originalText); 383 } 384 while (wordStart <= end && wordEnd != BreakIterator.DONE 385 && wordStart != BreakIterator.DONE) { 386 if (wordEnd >= start && wordEnd > wordStart) { 387 final String query = originalText.subSequence(wordStart, wordEnd).toString(); 388 final TextInfo ti = new TextInfo(query, cookie, query.hashCode()); 389 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 390 if (DBG) { 391 Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); 392 } 393 } 394 wordEnd = wordIterator.following(wordEnd); 395 if (wordEnd == BreakIterator.DONE) { 396 break; 397 } 398 wordStart = wordIterator.getBeginning(wordEnd); 399 } 400 if (originalText.length() >= SpellChecker.WORD_ITERATOR_INTERVAL 401 && wordItems.size() >= 2) { 402 if (DBG) { 403 Log.w(TAG, "Remove possibly divided word: " 404 + wordItems.get(0).mTextInfo.getText()); 405 } 406 wordItems.remove(0); 407 } 408 return new SentenceTextInfoParams(originalTextInfo, wordItems); 409 } 410 411 public static SentenceSuggestionsInfo reconstructSuggestions( 412 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 413 if (results == null || results.length == 0) { 414 return null; 415 } 416 if (DBG) { 417 Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); 418 } 419 if (originalTextInfoParams == null) { 420 if (DBG) { 421 Log.w(TAG, "Adapter: originalTextInfoParams is null."); 422 } 423 return null; 424 } 425 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 426 final int originalSequence = 427 originalTextInfoParams.mOriginalTextInfo.getSequence(); 428 429 final int querySize = originalTextInfoParams.mSize; 430 final int[] offsets = new int[querySize]; 431 final int[] lengths = new int[querySize]; 432 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 433 for (int i = 0; i < querySize; ++i) { 434 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 435 SuggestionsInfo result = null; 436 for (int j = 0; j < results.length; ++j) { 437 final SuggestionsInfo cur = results[j]; 438 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 439 result = cur; 440 result.setCookieAndSequence(originalCookie, originalSequence); 441 break; 442 } 443 } 444 offsets[i] = item.mStart; 445 lengths[i] = item.mLength; 446 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 447 if (DBG) { 448 final int size = reconstructedSuggestions[i].getSuggestionsCount(); 449 Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " 450 + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) 451 : "<none>") + ", offset = " + offsets[i] + ", length = " 452 + lengths[i]); 453 } 454 } 455 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 456 } 457 } 458} 459