blob: bd1b44ca18d015bb3fd4a0fbf39a85d1f15db8e4 [file] [log] [blame]
/*
* 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.ISpellCheckerServiceCallback;
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 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.
*
* <p>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}.
* </p>
*
* <h3>Returning spell check results</h3>
*
* <p>{@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.
* </p>
*
* <p>Please note that {@link SpellCheckerService.Session#getLocale()} does not return a valid
* locale before {@link SpellCheckerService.Session#onCreate()} </p>
*
*/
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<SentenceLevelAdapter.SentenceWordItem> 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<SpellCheckerService> mInternalServiceRef;
public SpellCheckerServiceBinder(SpellCheckerService service) {
mInternalServiceRef = new WeakReference<SpellCheckerService>(service);
}
/**
* Called from the system when an application is requesting a new spell checker session.
*
* <p>Note: This is an internal protocol used by the system to establish spell checker
* sessions, which is not guaranteed to be stable and is subject to change.</p>
*
* @param locale locale to be returned from {@link Session#getLocale()}
* @param listener IPC channel object to be used to implement
* {@link Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and
* {@link Session#onGetSuggestions(TextInfo, int)}
* @param bundle bundle to be returned from {@link Session#getBundle()}
* @param callback IPC channel to return the result to the caller in an asynchronous manner
*/
@Override
public void getISpellCheckerSession(
String locale, ISpellCheckerSessionListener listener, Bundle bundle,
ISpellCheckerServiceCallback callback) {
final SpellCheckerService service = mInternalServiceRef.get();
final InternalISpellCheckerSession internalSession;
if (service == null) {
// If the owner SpellCheckerService object was already destroyed and got GC-ed,
// the weak-reference returns null and we should just ignore this request.
internalSession = null;
} else {
final Session session = service.createSession();
internalSession =
new InternalISpellCheckerSession(locale, listener, bundle, session);
session.onCreate();
}
try {
callback.onSessionCreated(internalSession);
} catch (RemoteException e) {
}
}
}
/**
* 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<SentenceWordItem> mItems;
final int mSize;
public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> 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<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>();
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 CharSequence query = originalText.subSequence(wordStart, wordEnd);
final TextInfo ti = new TextInfo(query, 0, query.length(), 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)
: "<none>") + ", offset = " + offsets[i] + ", length = "
+ lengths[i]);
}
}
return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths);
}
}
}