/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package android.service.textservice; import com.android.internal.textservice.ISpellCheckerService; import com.android.internal.textservice.ISpellCheckerSession; import com.android.internal.textservice.ISpellCheckerSessionListener; import android.app.Service; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.text.TextUtils; import android.text.method.WordIterator; import android.util.Log; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import android.widget.SpellChecker; import java.lang.ref.WeakReference; import java.text.BreakIterator; import java.util.ArrayList; import java.util.Locale; /** * SpellCheckerService provides an abstract base class for a spell checker. * This class combines a service to the system with the spell checker service interface that * spell checker must implement. * *

In addition to the normal Service lifecycle methods, this class * introduces a new specific callback that subclasses should override * {@link #createSession()} to provide a spell checker session that is corresponding * to requested language and so on. The spell checker session returned by this method * should extend {@link SpellCheckerService.Session}. *

* *

Returning spell check results

* *

{@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} * should return spell check results. * It receives {@link android.view.textservice.TextInfo} and returns * {@link android.view.textservice.SuggestionsInfo} for the input. * You may want to override * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} for * better performance and quality. *

* *

Please note that {@link SpellCheckerService.Session#getLocale()} does not return a valid * locale before {@link SpellCheckerService.Session#onCreate()}

* */ public abstract class SpellCheckerService extends Service { private static final String TAG = SpellCheckerService.class.getSimpleName(); private static final boolean DBG = false; public static final String SERVICE_INTERFACE = "android.service.textservice.SpellCheckerService"; private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this); /** * Implement to return the implementation of the internal spell checker * service interface. Subclasses should not override. */ @Override public final IBinder onBind(final Intent intent) { if (DBG) { Log.w(TAG, "onBind"); } return mBinder; } /** * Factory method to create a spell checker session impl * @return SpellCheckerSessionImpl which should be overridden by a concrete implementation. */ public abstract Session createSession(); /** * This abstract class should be overridden by a concrete implementation of a spell checker. */ public static abstract class Session { private InternalISpellCheckerSession mInternalSession; private volatile SentenceLevelAdapter mSentenceLevelAdapter; /** * @hide */ public final void setInternalISpellCheckerSession(InternalISpellCheckerSession session) { mInternalSession = session; } /** * This is called after the class is initialized, at which point it knows it can call * getLocale() etc... */ public abstract void onCreate(); /** * Get suggestions for specified text in TextInfo. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. * @param textInfo the text metadata * @param suggestionsLimit the maximum number of suggestions to be returned * @return SuggestionsInfo which contains suggestions for textInfo */ public abstract SuggestionsInfo onGetSuggestions(TextInfo textInfo, int suggestionsLimit); /** * A batch process of onGetSuggestions. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. * @param textInfos an array of the text metadata * @param suggestionsLimit the maximum number of suggestions to be returned * @param sequentialWords true if textInfos can be treated as sequential words. * @return an array of {@link SentenceSuggestionsInfo} returned by * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} */ public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { final int length = textInfos.length; final SuggestionsInfo[] retval = new SuggestionsInfo[length]; for (int i = 0; i < length; ++i) { retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit); retval[i].setCookieAndSequence( textInfos[i].getCookie(), textInfos[i].getSequence()); } return retval; } /** * Get sentence suggestions for specified texts in an array of TextInfo. * The default implementation splits the input text to words and returns * {@link SentenceSuggestionsInfo} which contains suggestions for each word. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. * When you override this method, make sure that suggestionsLimit is applied to suggestions * that share the same start position and length. * @param textInfos an array of the text metadata * @param suggestionsLimit the maximum number of suggestions to be returned * @return an array of {@link SentenceSuggestionsInfo} returned by * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} */ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { if (textInfos == null || textInfos.length == 0) { return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; } if (DBG) { Log.d(TAG, "onGetSentenceSuggestionsMultiple: + " + textInfos.length + ", " + suggestionsLimit); } if (mSentenceLevelAdapter == null) { synchronized(this) { if (mSentenceLevelAdapter == null) { final String localeStr = getLocale(); if (!TextUtils.isEmpty(localeStr)) { mSentenceLevelAdapter = new SentenceLevelAdapter(new Locale(localeStr)); } } } } if (mSentenceLevelAdapter == null) { return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; } final int infosSize = textInfos.length; final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; for (int i = 0; i < infosSize; ++i) { final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = mSentenceLevelAdapter.getSplitWords(textInfos[i]); final ArrayList mItems = textInfoParams.mItems; final int itemsSize = mItems.size(); final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; for (int j = 0; j < itemsSize; ++j) { splitTextInfos[j] = mItems.get(j).mTextInfo; } retval[i] = SentenceLevelAdapter.reconstructSuggestions( textInfoParams, onGetSuggestionsMultiple( splitTextInfos, suggestionsLimit, true)); } return retval; } /** * Request to abort all tasks executed in SpellChecker. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. */ public void onCancel() {} /** * Request to close this session. * This function will run on the incoming IPC thread. * So, this is not called on the main thread, * but will be called in series on another thread. */ public void onClose() {} /** * @return Locale for this session */ public String getLocale() { return mInternalSession.getLocale(); } /** * @return Bundle for this session */ public Bundle getBundle() { return mInternalSession.getBundle(); } } // Preventing from exposing ISpellCheckerSession.aidl, create an internal class. private static class InternalISpellCheckerSession extends ISpellCheckerSession.Stub { private ISpellCheckerSessionListener mListener; private final Session mSession; private final String mLocale; private final Bundle mBundle; public InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, Bundle bundle, Session session) { mListener = listener; mSession = session; mLocale = locale; mBundle = bundle; session.setInternalISpellCheckerSession(this); } @Override public void onGetSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { int pri = Process.getThreadPriority(Process.myTid()); try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); mListener.onGetSuggestions( mSession.onGetSuggestionsMultiple( textInfos, suggestionsLimit, sequentialWords)); } catch (RemoteException e) { } finally { Process.setThreadPriority(pri); } } @Override public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { try { mListener.onGetSentenceSuggestions( mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit)); } catch (RemoteException e) { } } @Override public void onCancel() { int pri = Process.getThreadPriority(Process.myTid()); try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); mSession.onCancel(); } finally { Process.setThreadPriority(pri); } } @Override public void onClose() { int pri = Process.getThreadPriority(Process.myTid()); try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); mSession.onClose(); } finally { Process.setThreadPriority(pri); mListener = null; } } public String getLocale() { return mLocale; } public Bundle getBundle() { return mBundle; } } private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub { private final WeakReference mInternalServiceRef; public SpellCheckerServiceBinder(SpellCheckerService service) { mInternalServiceRef = new WeakReference(service); } @Override public ISpellCheckerSession getISpellCheckerSession( String locale, ISpellCheckerSessionListener listener, Bundle bundle) { final SpellCheckerService service = mInternalServiceRef.get(); if (service == null) return null; final Session session = service.createSession(); final InternalISpellCheckerSession internalSession = new InternalISpellCheckerSession(locale, listener, bundle, session); session.onCreate(); return internalSession; } } /** * Adapter class to accommodate word level spell checking APIs to sentence level spell checking * APIs used in * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} */ private static class SentenceLevelAdapter { public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = new SentenceSuggestionsInfo[] {}; private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); /** * Container for split TextInfo parameters */ public static class SentenceWordItem { public final TextInfo mTextInfo; public final int mStart; public final int mLength; public SentenceWordItem(TextInfo ti, int start, int end) { mTextInfo = ti; mStart = start; mLength = end - start; } } /** * Container for originally queried TextInfo and parameters */ public static class SentenceTextInfoParams { final TextInfo mOriginalTextInfo; final ArrayList mItems; final int mSize; public SentenceTextInfoParams(TextInfo ti, ArrayList items) { mOriginalTextInfo = ti; mItems = items; mSize = items.size(); } } private final WordIterator mWordIterator; public SentenceLevelAdapter(Locale locale) { mWordIterator = new WordIterator(locale); } private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { final WordIterator wordIterator = mWordIterator; final CharSequence originalText = originalTextInfo.getText(); final int cookie = originalTextInfo.getCookie(); final int start = 0; final int end = originalText.length(); final ArrayList wordItems = new ArrayList(); wordIterator.setCharSequence(originalText, 0, originalText.length()); int wordEnd = wordIterator.following(start); int wordStart = wordIterator.getBeginning(wordEnd); if (DBG) { Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " + wordEnd + "\n" + originalText); } while (wordStart <= end && wordEnd != BreakIterator.DONE && wordStart != BreakIterator.DONE) { if (wordEnd >= start && wordEnd > wordStart) { final String query = originalText.subSequence(wordStart, wordEnd).toString(); final TextInfo ti = new TextInfo(query, cookie, query.hashCode()); wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); if (DBG) { Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); } } wordEnd = wordIterator.following(wordEnd); if (wordEnd == BreakIterator.DONE) { break; } wordStart = wordIterator.getBeginning(wordEnd); } return new SentenceTextInfoParams(originalTextInfo, wordItems); } public static SentenceSuggestionsInfo reconstructSuggestions( SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { if (results == null || results.length == 0) { return null; } if (DBG) { Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); } if (originalTextInfoParams == null) { if (DBG) { Log.w(TAG, "Adapter: originalTextInfoParams is null."); } return null; } final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); final int originalSequence = originalTextInfoParams.mOriginalTextInfo.getSequence(); final int querySize = originalTextInfoParams.mSize; final int[] offsets = new int[querySize]; final int[] lengths = new int[querySize]; final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; for (int i = 0; i < querySize; ++i) { final SentenceWordItem item = originalTextInfoParams.mItems.get(i); SuggestionsInfo result = null; for (int j = 0; j < results.length; ++j) { final SuggestionsInfo cur = results[j]; if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { result = cur; result.setCookieAndSequence(originalCookie, originalSequence); break; } } offsets[i] = item.mStart; lengths[i] = item.mLength; reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; if (DBG) { final int size = reconstructedSuggestions[i].getSuggestionsCount(); Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) : "") + ", offset = " + offsets[i] + ", length = " + lengths[i]); } } return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); } } }