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