blob: 77b22ed1faae17fb2febb851d2137116a1e9e02f [file] [log] [blame]
satok988323c2011-06-22 16:38:13 +09001/*
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;
satok53578062011-08-03 16:08:59 +090025import android.os.Bundle;
satok988323c2011-06-22 16:38:13 +090026import android.os.IBinder;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -080027import android.os.Process;
satok988323c2011-06-22 16:38:13 +090028import android.os.RemoteException;
satokc7ee1b92012-04-11 20:40:07 +090029import android.text.TextUtils;
30import android.text.method.WordIterator;
satok6be6d752011-07-28 20:40:38 +090031import android.util.Log;
satokd404fe12012-02-22 06:38:18 +090032import android.view.textservice.SentenceSuggestionsInfo;
satok988323c2011-06-22 16:38:13 +090033import android.view.textservice.SuggestionsInfo;
34import android.view.textservice.TextInfo;
satokc7ee1b92012-04-11 20:40:07 +090035import android.widget.SpellChecker;
satok988323c2011-06-22 16:38:13 +090036
37import java.lang.ref.WeakReference;
satokc7ee1b92012-04-11 20:40:07 +090038import java.text.BreakIterator;
39import java.util.ArrayList;
40import java.util.Locale;
satok988323c2011-06-22 16:38:13 +090041
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.
satok44b75032011-10-14 14:48:59 +090046 *
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 *
satok988323c2011-06-22 16:38:13 +090068 */
69public abstract class SpellCheckerService extends Service {
70 private static final String TAG = SpellCheckerService.class.getSimpleName();
satok6be6d752011-07-28 20:40:38 +090071 private static final boolean DBG = false;
satok142d7572011-07-25 11:01:49 +090072 public static final String SERVICE_INTERFACE =
73 "android.service.textservice.SpellCheckerService";
satok988323c2011-06-22 16:38:13 +090074
75 private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this);
76
satok988323c2011-06-22 16:38:13 +090077
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) {
satok6be6d752011-07-28 20:40:38 +090084 if (DBG) {
85 Log.w(TAG, "onBind");
86 }
satok988323c2011-06-22 16:38:13 +090087 return mBinder;
88 }
89
satok53578062011-08-03 16:08:59 +090090 /**
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();
satok988323c2011-06-22 16:38:13 +090095
satok53578062011-08-03 16:08:59 +090096 /**
97 * This abstract class should be overridden by a concrete implementation of a spell checker.
98 */
satok117999d2011-09-02 17:55:43 +090099 public static abstract class Session {
satok53578062011-08-03 16:08:59 +0900100 private InternalISpellCheckerSession mInternalSession;
satokc7ee1b92012-04-11 20:40:07 +0900101 private volatile SentenceLevelAdapter mSentenceLevelAdapter;
satok53578062011-08-03 16:08:59 +0900102
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
satok6183cd62012-03-27 12:08:29 +0900122 * @param suggestionsLimit the maximum number of suggestions to be returned
satok44b75032011-10-14 14:48:59 +0900123 * @return SuggestionsInfo which contains suggestions for textInfo
satok53578062011-08-03 16:08:59 +0900124 */
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
satok6183cd62012-03-27 12:08:29 +0900133 * @param suggestionsLimit the maximum number of suggestions to be returned
satok53578062011-08-03 16:08:59 +0900134 * @param sequentialWords true if textInfos can be treated as sequential words.
satok6183cd62012-03-27 12:08:29 +0900135 * @return an array of {@link SentenceSuggestionsInfo} returned by
136 * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
satok53578062011-08-03 16:08:59 +0900137 */
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 /**
satok431ea842012-03-29 18:26:33 +0900151 * Get sentence suggestions for specified texts in an array of TextInfo.
satokc7ee1b92012-04-11 20:40:07 +0900152 * The default implementation splits the input text to words and returns
153 * {@link SentenceSuggestionsInfo} which contains suggestions for each word.
satok431ea842012-03-29 18:26:33 +0900154 * 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.
satok0dc1f642011-11-18 11:27:10 +0900157 * When you override this method, make sure that suggestionsLimit is applied to suggestions
158 * that share the same start position and length.
satok6183cd62012-03-27 12:08:29 +0900159 * @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)}
satok0dc1f642011-11-18 11:27:10 +0900163 */
satokd404fe12012-02-22 06:38:18 +0900164 public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
satok0dc1f642011-11-18 11:27:10 +0900165 int suggestionsLimit) {
satokc7ee1b92012-04-11 20:40:07 +0900166 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));
satok0dc1f642011-11-18 11:27:10 +0900201 }
202 return retval;
203 }
204
205 /**
satok53578062011-08-03 16:08:59 +0900206 * 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 /**
satok74061ff2011-11-02 11:20:33 +0900214 * 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 /**
satok53578062011-08-03 16:08:59 +0900222 * @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 {
satok74061ff2011-11-02 11:20:33 +0900238 private ISpellCheckerSessionListener mListener;
satok53578062011-08-03 16:08:59 +0900239 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) {
satok988323c2011-06-22 16:38:13 +0900245 mListener = listener;
satok53578062011-08-03 16:08:59 +0900246 mSession = session;
247 mLocale = locale;
248 mBundle = bundle;
249 session.setInternalISpellCheckerSession(this);
satok988323c2011-06-22 16:38:13 +0900250 }
251
252 @Override
satok53578062011-08-03 16:08:59 +0900253 public void onGetSuggestionsMultiple(
satok988323c2011-06-22 16:38:13 +0900254 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800255 int pri = Process.getThreadPriority(Process.myTid());
satok988323c2011-06-22 16:38:13 +0900256 try {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800257 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
satok988323c2011-06-22 16:38:13 +0900258 mListener.onGetSuggestions(
satok53578062011-08-03 16:08:59 +0900259 mSession.onGetSuggestionsMultiple(
260 textInfos, suggestionsLimit, sequentialWords));
satok988323c2011-06-22 16:38:13 +0900261 } catch (RemoteException e) {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800262 } finally {
263 Process.setThreadPriority(pri);
satok988323c2011-06-22 16:38:13 +0900264 }
265 }
266
267 @Override
satokd404fe12012-02-22 06:38:18 +0900268 public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
satok0dc1f642011-11-18 11:27:10 +0900269 try {
satokd404fe12012-02-22 06:38:18 +0900270 mListener.onGetSentenceSuggestions(
271 mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit));
satok0dc1f642011-11-18 11:27:10 +0900272 } catch (RemoteException e) {
273 }
274 }
275
276 @Override
satok53578062011-08-03 16:08:59 +0900277 public void onCancel() {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800278 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 }
satok53578062011-08-03 16:08:59 +0900285 }
286
satok74061ff2011-11-02 11:20:33 +0900287 @Override
288 public void onClose() {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800289 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 }
satok74061ff2011-11-02 11:20:33 +0900297 }
298
satok53578062011-08-03 16:08:59 +0900299 public String getLocale() {
300 return mLocale;
301 }
302
303 public Bundle getBundle() {
304 return mBundle;
satok988323c2011-06-22 16:38:13 +0900305 }
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(
satok53578062011-08-03 16:08:59 +0900317 String locale, ISpellCheckerSessionListener listener, Bundle bundle) {
satok988323c2011-06-22 16:38:13 +0900318 final SpellCheckerService service = mInternalServiceRef.get();
319 if (service == null) return null;
satok53578062011-08-03 16:08:59 +0900320 final Session session = service.createSession();
321 final InternalISpellCheckerSession internalSession =
322 new InternalISpellCheckerSession(locale, listener, bundle, session);
323 session.onCreate();
324 return internalSession;
satok988323c2011-06-22 16:38:13 +0900325 }
326 }
satokc7ee1b92012-04-11 20:40:07 +0900327
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 }
satokc7ee1b92012-04-11 20:40:07 +0900400 return new SentenceTextInfoParams(originalTextInfo, wordItems);
401 }
402
403 public static SentenceSuggestionsInfo reconstructSuggestions(
404 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) {
405 if (results == null || results.length == 0) {
406 return null;
407 }
408 if (DBG) {
409 Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length);
410 }
411 if (originalTextInfoParams == null) {
412 if (DBG) {
413 Log.w(TAG, "Adapter: originalTextInfoParams is null.");
414 }
415 return null;
416 }
417 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie();
418 final int originalSequence =
419 originalTextInfoParams.mOriginalTextInfo.getSequence();
420
421 final int querySize = originalTextInfoParams.mSize;
422 final int[] offsets = new int[querySize];
423 final int[] lengths = new int[querySize];
424 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize];
425 for (int i = 0; i < querySize; ++i) {
426 final SentenceWordItem item = originalTextInfoParams.mItems.get(i);
427 SuggestionsInfo result = null;
428 for (int j = 0; j < results.length; ++j) {
429 final SuggestionsInfo cur = results[j];
430 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) {
431 result = cur;
432 result.setCookieAndSequence(originalCookie, originalSequence);
433 break;
434 }
435 }
436 offsets[i] = item.mStart;
437 lengths[i] = item.mLength;
438 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO;
439 if (DBG) {
440 final int size = reconstructedSuggestions[i].getSuggestionsCount();
441 Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = "
442 + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0)
443 : "<none>") + ", offset = " + offsets[i] + ", length = "
444 + lengths[i]);
445 }
446 }
447 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths);
448 }
449 }
satok988323c2011-06-22 16:38:13 +0900450}