SpellCheckerService.java revision 7fa65eef203c4ed3ce00ddef96ccf311d3bfb58c
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.ISpellCheckerServiceCallback;
21import com.android.internal.textservice.ISpellCheckerSession;
22import com.android.internal.textservice.ISpellCheckerSessionListener;
23
24import android.app.Service;
25import android.content.Intent;
26import android.os.Bundle;
27import android.os.IBinder;
28import android.os.Process;
29import android.os.RemoteException;
30import android.text.TextUtils;
31import android.text.method.WordIterator;
32import android.util.Log;
33import android.view.textservice.SentenceSuggestionsInfo;
34import android.view.textservice.SuggestionsInfo;
35import android.view.textservice.TextInfo;
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        /**
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
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;
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;
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;
389        public SentenceLevelAdapter(Locale locale) {
390            mWordIterator = new WordIterator(locale);
391        }
392
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
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