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 17 package android.service.textservice; 18 19 import com.android.internal.textservice.ISpellCheckerService; 20 import com.android.internal.textservice.ISpellCheckerServiceCallback; 21 import com.android.internal.textservice.ISpellCheckerSession; 22 import com.android.internal.textservice.ISpellCheckerSessionListener; 23 24 import android.app.Service; 25 import android.content.Intent; 26 import android.os.Bundle; 27 import android.os.IBinder; 28 import android.os.Process; 29 import android.os.RemoteException; 30 import android.text.TextUtils; 31 import android.text.method.WordIterator; 32 import android.util.Log; 33 import android.view.textservice.SentenceSuggestionsInfo; 34 import android.view.textservice.SuggestionsInfo; 35 import android.view.textservice.TextInfo; 36 37 import java.lang.ref.WeakReference; 38 import java.text.BreakIterator; 39 import java.util.ArrayList; 40 import 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 */ 69 public 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 onBind(final Intent intent)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 */ createSession()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 */ setInternalISpellCheckerSession(InternalISpellCheckerSession session)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 */ onCreate()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 */ onGetSuggestions(TextInfo textInfo, int suggestionsLimit)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 */ onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)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 */ onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)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 */ onCancel()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 */ onClose()219 public void onClose() {} 220 221 /** 222 * @return Locale for this session 223 */ getLocale()224 public String getLocale() { 225 return mInternalSession.getLocale(); 226 } 227 228 /** 229 * @return Bundle for this session 230 */ getBundle()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 InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, Bundle bundle, Session session)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 onGetSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)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 onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)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 onCancel()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 onClose()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 getLocale()299 public String getLocale() { 300 return mLocale; 301 } 302 getBundle()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 SpellCheckerServiceBinder(SpellCheckerService service)311 public SpellCheckerServiceBinder(SpellCheckerService service) { 312 mInternalServiceRef = new WeakReference<SpellCheckerService>(service); 313 } 314 315 /** 316 * Called from the system when an application is requesting a new spell checker session. 317 * 318 * <p>Note: This is an internal protocol used by the system to establish spell checker 319 * sessions, which is not guaranteed to be stable and is subject to change.</p> 320 * 321 * @param locale locale to be returned from {@link Session#getLocale()} 322 * @param listener IPC channel object to be used to implement 323 * {@link Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and 324 * {@link Session#onGetSuggestions(TextInfo, int)} 325 * @param bundle bundle to be returned from {@link Session#getBundle()} 326 * @param callback IPC channel to return the result to the caller in an asynchronous manner 327 */ 328 @Override getISpellCheckerSession( String locale, ISpellCheckerSessionListener listener, Bundle bundle, ISpellCheckerServiceCallback callback)329 public void getISpellCheckerSession( 330 String locale, ISpellCheckerSessionListener listener, Bundle bundle, 331 ISpellCheckerServiceCallback callback) { 332 final SpellCheckerService service = mInternalServiceRef.get(); 333 final InternalISpellCheckerSession internalSession; 334 if (service == null) { 335 // If the owner SpellCheckerService object was already destroyed and got GC-ed, 336 // the weak-reference returns null and we should just ignore this request. 337 internalSession = null; 338 } else { 339 final Session session = service.createSession(); 340 internalSession = 341 new InternalISpellCheckerSession(locale, listener, bundle, session); 342 session.onCreate(); 343 } 344 try { 345 callback.onSessionCreated(internalSession); 346 } catch (RemoteException e) { 347 } 348 } 349 } 350 351 /** 352 * Adapter class to accommodate word level spell checking APIs to sentence level spell checking 353 * APIs used in 354 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} 355 */ 356 private static class SentenceLevelAdapter { 357 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 358 new SentenceSuggestionsInfo[] {}; 359 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 360 /** 361 * Container for split TextInfo parameters 362 */ 363 public static class SentenceWordItem { 364 public final TextInfo mTextInfo; 365 public final int mStart; 366 public final int mLength; SentenceWordItem(TextInfo ti, int start, int end)367 public SentenceWordItem(TextInfo ti, int start, int end) { 368 mTextInfo = ti; 369 mStart = start; 370 mLength = end - start; 371 } 372 } 373 374 /** 375 * Container for originally queried TextInfo and parameters 376 */ 377 public static class SentenceTextInfoParams { 378 final TextInfo mOriginalTextInfo; 379 final ArrayList<SentenceWordItem> mItems; 380 final int mSize; SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items)381 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 382 mOriginalTextInfo = ti; 383 mItems = items; 384 mSize = items.size(); 385 } 386 } 387 388 private final WordIterator mWordIterator; SentenceLevelAdapter(Locale locale)389 public SentenceLevelAdapter(Locale locale) { 390 mWordIterator = new WordIterator(locale); 391 } 392 getSplitWords(TextInfo originalTextInfo)393 private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 394 final WordIterator wordIterator = mWordIterator; 395 final CharSequence originalText = originalTextInfo.getText(); 396 final int cookie = originalTextInfo.getCookie(); 397 final int start = 0; 398 final int end = originalText.length(); 399 final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); 400 wordIterator.setCharSequence(originalText, 0, originalText.length()); 401 int wordEnd = wordIterator.following(start); 402 int wordStart = wordIterator.getBeginning(wordEnd); 403 if (DBG) { 404 Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " 405 + wordEnd + "\n" + originalText); 406 } 407 while (wordStart <= end && wordEnd != BreakIterator.DONE 408 && wordStart != BreakIterator.DONE) { 409 if (wordEnd >= start && wordEnd > wordStart) { 410 final CharSequence query = originalText.subSequence(wordStart, wordEnd); 411 final TextInfo ti = new TextInfo(query, 0, query.length(), cookie, 412 query.hashCode()); 413 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 414 if (DBG) { 415 Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); 416 } 417 } 418 wordEnd = wordIterator.following(wordEnd); 419 if (wordEnd == BreakIterator.DONE) { 420 break; 421 } 422 wordStart = wordIterator.getBeginning(wordEnd); 423 } 424 return new SentenceTextInfoParams(originalTextInfo, wordItems); 425 } 426 reconstructSuggestions( SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results)427 public static SentenceSuggestionsInfo reconstructSuggestions( 428 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 429 if (results == null || results.length == 0) { 430 return null; 431 } 432 if (DBG) { 433 Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); 434 } 435 if (originalTextInfoParams == null) { 436 if (DBG) { 437 Log.w(TAG, "Adapter: originalTextInfoParams is null."); 438 } 439 return null; 440 } 441 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 442 final int originalSequence = 443 originalTextInfoParams.mOriginalTextInfo.getSequence(); 444 445 final int querySize = originalTextInfoParams.mSize; 446 final int[] offsets = new int[querySize]; 447 final int[] lengths = new int[querySize]; 448 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 449 for (int i = 0; i < querySize; ++i) { 450 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 451 SuggestionsInfo result = null; 452 for (int j = 0; j < results.length; ++j) { 453 final SuggestionsInfo cur = results[j]; 454 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 455 result = cur; 456 result.setCookieAndSequence(originalCookie, originalSequence); 457 break; 458 } 459 } 460 offsets[i] = item.mStart; 461 lengths[i] = item.mLength; 462 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 463 if (DBG) { 464 final int size = reconstructedSuggestions[i].getSuggestionsCount(); 465 Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " 466 + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) 467 : "<none>") + ", offset = " + offsets[i] + ", length = " 468 + lengths[i]); 469 } 470 } 471 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 472 } 473 } 474 } 475