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 CharSequence query = originalText.subSequence(wordStart, wordEnd);
387                    final TextInfo ti = new TextInfo(query, 0, query.length(), cookie,
388                            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            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    }
450}
451