summaryrefslogtreecommitdiff
path: root/android/view/textservice
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/view/textservice
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \ --bid 4335822 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4335822.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/view/textservice')
-rw-r--r--android/view/textservice/SentenceSuggestionsInfo.java144
-rw-r--r--android/view/textservice/SpellCheckerInfo.java290
-rw-r--r--android/view/textservice/SpellCheckerSession.java572
-rw-r--r--android/view/textservice/SpellCheckerSubtype.java321
-rw-r--r--android/view/textservice/SuggestionsInfo.java185
-rw-r--r--android/view/textservice/TextInfo.java161
-rw-r--r--android/view/textservice/TextServicesManager.java236
7 files changed, 1909 insertions, 0 deletions
diff --git a/android/view/textservice/SentenceSuggestionsInfo.java b/android/view/textservice/SentenceSuggestionsInfo.java
new file mode 100644
index 00000000..afd62eb5
--- /dev/null
+++ b/android/view/textservice/SentenceSuggestionsInfo.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 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.view.textservice;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * This class contains a metadata of suggestions returned from a text service
+ * (e.g. {@link android.service.textservice.SpellCheckerService}).
+ * The text service uses this class to return the suggestions
+ * for a sentence. See {@link SuggestionsInfo} which is used for suggestions for a word.
+ * This class extends the functionality of {@link SuggestionsInfo} as far as this class enables
+ * you to put multiple {@link SuggestionsInfo}s on a sentence with the offsets and the lengths
+ * of all {@link SuggestionsInfo}s.
+ */
+public final class SentenceSuggestionsInfo implements Parcelable {
+
+ private final SuggestionsInfo[] mSuggestionsInfos;
+ private final int[] mOffsets;
+ private final int[] mLengths;
+
+ /**
+ * Constructor.
+ * @param suggestionsInfos from the text service
+ * @param offsets the array of offsets of suggestions
+ * @param lengths the array of lengths of suggestions
+ */
+ public SentenceSuggestionsInfo(
+ SuggestionsInfo[] suggestionsInfos, int[] offsets, int[] lengths) {
+ if (suggestionsInfos == null || offsets == null || lengths == null) {
+ throw new NullPointerException();
+ }
+ if (suggestionsInfos.length != offsets.length || offsets.length != lengths.length) {
+ throw new IllegalArgumentException();
+ }
+ final int infoSize = suggestionsInfos.length;
+ mSuggestionsInfos = Arrays.copyOf(suggestionsInfos, infoSize);
+ mOffsets = Arrays.copyOf(offsets, infoSize);
+ mLengths = Arrays.copyOf(lengths, infoSize);
+ }
+
+ public SentenceSuggestionsInfo(Parcel source) {
+ final int infoSize = source.readInt();
+ mSuggestionsInfos = new SuggestionsInfo[infoSize];
+ source.readTypedArray(mSuggestionsInfos, SuggestionsInfo.CREATOR);
+ mOffsets = new int[mSuggestionsInfos.length];
+ source.readIntArray(mOffsets);
+ mLengths = new int[mSuggestionsInfos.length];
+ source.readIntArray(mLengths);
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ final int infoSize = mSuggestionsInfos.length;
+ dest.writeInt(infoSize);
+ dest.writeTypedArray(mSuggestionsInfos, 0);
+ dest.writeIntArray(mOffsets);
+ dest.writeIntArray(mLengths);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @return the count of {@link SuggestionsInfo}s this instance holds.
+ */
+ public int getSuggestionsCount() {
+ return mSuggestionsInfos.length;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds.
+ * @return a {@link SuggestionsInfo} at the specified id
+ */
+ public SuggestionsInfo getSuggestionsInfoAt(int i) {
+ if (i >= 0 && i < mSuggestionsInfos.length) {
+ return mSuggestionsInfos[i];
+ }
+ return null;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the offset of the specified {@link SuggestionsInfo}
+ */
+ public int getOffsetAt(int i) {
+ if (i >= 0 && i < mOffsets.length) {
+ return mOffsets[i];
+ }
+ return -1;
+ }
+
+ /**
+ * @param i the id of {@link SuggestionsInfo}s this instance holds
+ * @return the length of the specified {@link SuggestionsInfo}
+ */
+ public int getLengthAt(int i) {
+ if (i >= 0 && i < mLengths.length) {
+ return mLengths[i];
+ }
+ return -1;
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<SentenceSuggestionsInfo> CREATOR
+ = new Parcelable.Creator<SentenceSuggestionsInfo>() {
+ @Override
+ public SentenceSuggestionsInfo createFromParcel(Parcel source) {
+ return new SentenceSuggestionsInfo(source);
+ }
+
+ @Override
+ public SentenceSuggestionsInfo[] newArray(int size) {
+ return new SentenceSuggestionsInfo[size];
+ }
+ };
+}
diff --git a/android/view/textservice/SpellCheckerInfo.java b/android/view/textservice/SpellCheckerInfo.java
new file mode 100644
index 00000000..7aa2c23a
--- /dev/null
+++ b/android/view/textservice/SpellCheckerInfo.java
@@ -0,0 +1,290 @@
+/*
+ * 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.view.textservice;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.PrintWriterPrinter;
+import android.util.Slog;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * This class is used to specify meta information of a spell checker.
+ */
+public final class SpellCheckerInfo implements Parcelable {
+ private static final String TAG = SpellCheckerInfo.class.getSimpleName();
+ private final ResolveInfo mService;
+ private final String mId;
+ private final int mLabel;
+
+ /**
+ * The spell checker setting activity's name, used by the system settings to
+ * launch the setting activity.
+ */
+ private final String mSettingsActivityName;
+
+ /**
+ * The array of subtypes.
+ */
+ private final ArrayList<SpellCheckerSubtype> mSubtypes = new ArrayList<>();
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Context context, ResolveInfo service)
+ throws XmlPullParserException, IOException {
+ mService = service;
+ ServiceInfo si = service.serviceInfo;
+ mId = new ComponentName(si.packageName, si.name).flattenToShortString();
+
+ final PackageManager pm = context.getPackageManager();
+ int label = 0;
+ String settingsActivityComponent = null;
+
+ XmlResourceParser parser = null;
+ try {
+ parser = si.loadXmlMetaData(pm, SpellCheckerSession.SERVICE_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException("No "
+ + SpellCheckerSession.SERVICE_META_DATA + " meta-data");
+ }
+
+ final Resources res = pm.getResourcesForApplication(si.applicationInfo);
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ int type;
+ while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ }
+
+ final String nodeName = parser.getName();
+ if (!"spell-checker".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with spell-checker tag");
+ }
+
+ TypedArray sa = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.SpellChecker);
+ label = sa.getResourceId(com.android.internal.R.styleable.SpellChecker_label, 0);
+ settingsActivityComponent = sa.getString(
+ com.android.internal.R.styleable.SpellChecker_settingsActivity);
+ sa.recycle();
+
+ final int depth = parser.getDepth();
+ // Parse all subtypes
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+ && type != XmlPullParser.END_DOCUMENT) {
+ if (type == XmlPullParser.START_TAG) {
+ final String subtypeNodeName = parser.getName();
+ if (!"subtype".equals(subtypeNodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data in spell-checker does not start with subtype tag");
+ }
+ final TypedArray a = res.obtainAttributes(
+ attrs, com.android.internal.R.styleable.SpellChecker_Subtype);
+ SpellCheckerSubtype subtype = new SpellCheckerSubtype(
+ a.getResourceId(com.android.internal.R.styleable
+ .SpellChecker_Subtype_label, 0),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeLocale),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_languageTag),
+ a.getString(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeExtraValue),
+ a.getInt(com.android.internal.R.styleable
+ .SpellChecker_Subtype_subtypeId, 0));
+ mSubtypes.add(subtype);
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Caught exception: " + e);
+ throw new XmlPullParserException(
+ "Unable to create context for: " + si.packageName);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ mLabel = label;
+ mSettingsActivityName = settingsActivityComponent;
+ }
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Parcel source) {
+ mLabel = source.readInt();
+ mId = source.readString();
+ mSettingsActivityName = source.readString();
+ mService = ResolveInfo.CREATOR.createFromParcel(source);
+ source.readTypedList(mSubtypes, SpellCheckerSubtype.CREATOR);
+ }
+
+ /**
+ * Return a unique ID for this spell checker. The ID is generated from
+ * the package and class name implementing the method.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Return the component of the service that implements.
+ */
+ public ComponentName getComponent() {
+ return new ComponentName(
+ mService.serviceInfo.packageName, mService.serviceInfo.name);
+ }
+
+ /**
+ * Return the .apk package that implements this.
+ */
+ public String getPackageName() {
+ return mService.serviceInfo.packageName;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mLabel);
+ dest.writeString(mId);
+ dest.writeString(mSettingsActivityName);
+ mService.writeToParcel(dest, flags);
+ dest.writeTypedList(mSubtypes);
+ }
+
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<SpellCheckerInfo> CREATOR
+ = new Parcelable.Creator<SpellCheckerInfo>() {
+ @Override
+ public SpellCheckerInfo createFromParcel(Parcel source) {
+ return new SpellCheckerInfo(source);
+ }
+
+ @Override
+ public SpellCheckerInfo[] newArray(int size) {
+ return new SpellCheckerInfo[size];
+ }
+ };
+
+ /**
+ * Load the user-displayed label for this spell checker.
+ *
+ * @param pm Supply a PackageManager used to load the spell checker's resources.
+ */
+ public CharSequence loadLabel(PackageManager pm) {
+ if (mLabel == 0 || pm == null) return "";
+ return pm.getText(getPackageName(), mLabel, mService.serviceInfo.applicationInfo);
+ }
+
+ /**
+ * Load the user-displayed icon for this spell checker.
+ *
+ * @param pm Supply a PackageManager used to load the spell checker's resources.
+ */
+ public Drawable loadIcon(PackageManager pm) {
+ return mService.loadIcon(pm);
+ }
+
+
+ /**
+ * Return the raw information about the Service implementing this
+ * spell checker. Do not modify the returned object.
+ */
+ public ServiceInfo getServiceInfo() {
+ return mService.serviceInfo;
+ }
+
+ /**
+ * Return the class name of an activity that provides a settings UI.
+ * You can launch this activity be starting it with
+ * an {@link android.content.Intent} whose action is MAIN and with an
+ * explicit {@link android.content.ComponentName}
+ * composed of {@link #getPackageName} and the class name returned here.
+ *
+ * <p>A null will be returned if there is no settings activity.
+ */
+ public String getSettingsActivity() {
+ return mSettingsActivityName;
+ }
+
+ /**
+ * Return the count of the subtypes.
+ */
+ public int getSubtypeCount() {
+ return mSubtypes.size();
+ }
+
+ /**
+ * Return the subtype at the specified index.
+ *
+ * @param index the index of the subtype to return.
+ */
+ public SpellCheckerSubtype getSubtypeAt(int index) {
+ return mSubtypes.get(index);
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ public void dump(final PrintWriter pw, final String prefix) {
+ pw.println(prefix + "mId=" + mId);
+ pw.println(prefix + "mSettingsActivityName=" + mSettingsActivityName);
+ pw.println(prefix + "Service:");
+ mService.dump(new PrintWriterPrinter(pw), prefix + " ");
+ final int N = getSubtypeCount();
+ for (int i = 0; i < N; i++) {
+ final SpellCheckerSubtype st = getSubtypeAt(i);
+ pw.println(prefix + " " + "Subtype #" + i + ":");
+ pw.println(prefix + " " + "locale=" + st.getLocale()
+ + " languageTag=" + st.getLanguageTag());
+ pw.println(prefix + " " + "extraValue=" + st.getExtraValue());
+ }
+ }
+}
diff --git a/android/view/textservice/SpellCheckerSession.java b/android/view/textservice/SpellCheckerSession.java
new file mode 100644
index 00000000..779eefb1
--- /dev/null
+++ b/android/view/textservice/SpellCheckerSession.java
@@ -0,0 +1,572 @@
+/*
+ * 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.view.textservice;
+
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.textservice.ISpellCheckerSession;
+import com.android.internal.textservice.ISpellCheckerSessionListener;
+import com.android.internal.textservice.ITextServicesManager;
+import com.android.internal.textservice.ITextServicesSessionListener;
+
+import dalvik.system.CloseGuard;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
+ *
+ *
+ * <a name="Applications"></a>
+ * <h3>Applications</h3>
+ *
+ * <p>In most cases, applications that are using the standard
+ * {@link android.widget.TextView} or its subclasses will have little they need
+ * to do to work well with spell checker services. The main things you need to
+ * be aware of are:</p>
+ *
+ * <ul>
+ * <li> Properly set the {@link android.R.attr#inputType} in your editable
+ * text views, so that the spell checker will have enough context to help the
+ * user in editing text in them.
+ * </ul>
+ *
+ * <p>For the rare people amongst us writing client applications that use the spell checker service
+ * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
+ * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
+ * service by yourself.</p>
+ *
+ * <h3>Security</h3>
+ *
+ * <p>There are a lot of security issues associated with spell checkers,
+ * since they could monitor all the text being sent to them
+ * through, for instance, {@link android.widget.TextView}.
+ * The Android spell checker framework also allows
+ * arbitrary third party spell checkers, so care must be taken to restrict their
+ * selection and interactions.</p>
+ *
+ * <p>Here are some key points about the security architecture behind the
+ * spell checker framework:</p>
+ *
+ * <ul>
+ * <li>Only the system is allowed to directly access a spell checker framework's
+ * {@link android.service.textservice.SpellCheckerService} interface, via the
+ * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is
+ * enforced in the system by not binding to a spell checker service that does
+ * not require this permission.
+ *
+ * <li>The user must explicitly enable a new spell checker in settings before
+ * they can be enabled, to confirm with the system that they know about it
+ * and want to make it available for use.
+ * </ul>
+ *
+ */
+public class SpellCheckerSession {
+ private static final String TAG = SpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+ /**
+ * Name under which a SpellChecker service component publishes information about itself.
+ * This meta-data must reference an XML resource.
+ **/
+ public static final String SERVICE_META_DATA = "android.view.textservice.scs";
+
+ private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
+ private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
+
+ private final InternalListener mInternalListener;
+ private final ITextServicesManager mTextServicesManager;
+ private final SpellCheckerInfo mSpellCheckerInfo;
+ private final SpellCheckerSessionListener mSpellCheckerSessionListener;
+ private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
+
+ private final CloseGuard mGuard = CloseGuard.get();
+
+ /** Handler that will execute the main tasks */
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ON_GET_SUGGESTION_MULTIPLE:
+ handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
+ break;
+ case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
+ handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Constructor
+ * @hide
+ */
+ public SpellCheckerSession(
+ SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) {
+ if (info == null || listener == null || tsm == null) {
+ throw new NullPointerException();
+ }
+ mSpellCheckerInfo = info;
+ mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
+ mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
+ mTextServicesManager = tsm;
+ mSpellCheckerSessionListener = listener;
+
+ mGuard.open("finishSession");
+ }
+
+ /**
+ * @return true if the connection to a text service of this session is disconnected and not
+ * alive.
+ */
+ public boolean isSessionDisconnected() {
+ return mSpellCheckerSessionListenerImpl.isDisconnected();
+ }
+
+ /**
+ * Get the spell checker service info this spell checker session has.
+ * @return SpellCheckerInfo for the specified locale.
+ */
+ public SpellCheckerInfo getSpellChecker() {
+ return mSpellCheckerInfo;
+ }
+
+ /**
+ * Cancel pending and running spell check tasks
+ */
+ public void cancel() {
+ mSpellCheckerSessionListenerImpl.cancel();
+ }
+
+ /**
+ * Finish this session and allow TextServicesManagerService to disconnect the bound spell
+ * checker.
+ */
+ public void close() {
+ mGuard.close();
+ try {
+ mSpellCheckerSessionListenerImpl.close();
+ mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
+ } catch (RemoteException e) {
+ // do nothing
+ }
+ }
+
+ /**
+ * Get suggestions from the specified sentences
+ * @param textInfos an array of text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ */
+ public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
+ mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
+ textInfos, suggestionsLimit);
+ }
+
+ /**
+ * Get candidate strings for a substring of the specified text.
+ * @param textInfo text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
+ */
+ @Deprecated
+ public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
+ getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
+ }
+
+ /**
+ * A batch process of getSuggestions
+ * @param textInfos an array of text metadata for a spell checker
+ * @param suggestionsLimit the maximum number of suggestions that will be returned
+ * @param sequentialWords true if textInfos can be treated as sequential words.
+ * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
+ */
+ @Deprecated
+ public void getSuggestions(
+ TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
+ if (DBG) {
+ Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
+ }
+ mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
+ textInfos, suggestionsLimit, sequentialWords);
+ }
+
+ private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
+ mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
+ }
+
+ private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) {
+ mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos);
+ }
+
+ private static final class SpellCheckerSessionListenerImpl
+ extends ISpellCheckerSessionListener.Stub {
+ private static final int TASK_CANCEL = 1;
+ private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
+ private static final int TASK_CLOSE = 3;
+ private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
+ private static String taskToString(int task) {
+ switch (task) {
+ case TASK_CANCEL:
+ return "TASK_CANCEL";
+ case TASK_GET_SUGGESTIONS_MULTIPLE:
+ return "TASK_GET_SUGGESTIONS_MULTIPLE";
+ case TASK_CLOSE:
+ return "TASK_CLOSE";
+ case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
+ return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
+ default:
+ return "Unexpected task=" + task;
+ }
+ }
+
+ private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>();
+ private Handler mHandler;
+
+ private static final int STATE_WAIT_CONNECTION = 0;
+ private static final int STATE_CONNECTED = 1;
+ private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
+ private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
+ private static String stateToString(int state) {
+ switch (state) {
+ case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
+ case STATE_CONNECTED: return "STATE_CONNECTED";
+ case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
+ case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
+ default: return "Unexpected state=" + state;
+ }
+ }
+ private int mState = STATE_WAIT_CONNECTION;
+
+ private ISpellCheckerSession mISpellCheckerSession;
+ private HandlerThread mThread;
+ private Handler mAsyncHandler;
+
+ public SpellCheckerSessionListenerImpl(Handler handler) {
+ mHandler = handler;
+ }
+
+ private static class SpellCheckerParams {
+ public final int mWhat;
+ public final TextInfo[] mTextInfos;
+ public final int mSuggestionsLimit;
+ public final boolean mSequentialWords;
+ public ISpellCheckerSession mSession;
+ public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
+ boolean sequentialWords) {
+ mWhat = what;
+ mTextInfos = textInfos;
+ mSuggestionsLimit = suggestionsLimit;
+ mSequentialWords = sequentialWords;
+ }
+ }
+
+ private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
+ boolean async) {
+ if (DBG) {
+ synchronized (this) {
+ Log.d(TAG, "entering processTask:"
+ + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
+ + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
+ + " mAsyncHandler=" + mAsyncHandler
+ + " mState=" + stateToString(mState));
+ }
+ }
+ if (async || mAsyncHandler == null) {
+ switch (scp.mWhat) {
+ case TASK_CANCEL:
+ try {
+ session.onCancel();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to cancel " + e);
+ }
+ break;
+ case TASK_GET_SUGGESTIONS_MULTIPLE:
+ try {
+ session.onGetSuggestionsMultiple(scp.mTextInfos,
+ scp.mSuggestionsLimit, scp.mSequentialWords);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get suggestions " + e);
+ }
+ break;
+ case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
+ try {
+ session.onGetSentenceSuggestionsMultiple(
+ scp.mTextInfos, scp.mSuggestionsLimit);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get suggestions " + e);
+ }
+ break;
+ case TASK_CLOSE:
+ try {
+ session.onClose();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to close " + e);
+ }
+ break;
+ }
+ } else {
+ // The interface is to a local object, so need to execute it
+ // asynchronously.
+ scp.mSession = session;
+ mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
+ }
+
+ if (scp.mWhat == TASK_CLOSE) {
+ // If we are closing, we want to clean up our state now even
+ // if it is pending as an async operation.
+ synchronized (this) {
+ processCloseLocked();
+ }
+ }
+ }
+
+ private void processCloseLocked() {
+ if (DBG) Log.d(TAG, "entering processCloseLocked:"
+ + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
+ + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
+ + " mState=" + stateToString(mState));
+ mISpellCheckerSession = null;
+ if (mThread != null) {
+ mThread.quit();
+ }
+ mHandler = null;
+ mPendingTasks.clear();
+ mThread = null;
+ mAsyncHandler = null;
+ switch (mState) {
+ case STATE_WAIT_CONNECTION:
+ mState = STATE_CLOSED_BEFORE_CONNECTION;
+ break;
+ case STATE_CONNECTED:
+ mState = STATE_CLOSED_AFTER_CONNECTION;
+ break;
+ default:
+ Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
+ stateToString(mState));
+ break;
+ }
+ }
+
+ public void onServiceConnected(ISpellCheckerSession session) {
+ synchronized (this) {
+ switch (mState) {
+ case STATE_WAIT_CONNECTION:
+ // OK, go ahead.
+ break;
+ case STATE_CLOSED_BEFORE_CONNECTION:
+ // This is possible, and not an error. The client no longer is interested
+ // in this connection. OK to ignore.
+ if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
+ + " already closed.");
+ return;
+ default:
+ Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
+ + stateToString(mState));
+ return;
+ }
+ if (session == null) {
+ Log.e(TAG, "ignoring onServiceConnected due to session=null");
+ return;
+ }
+ mISpellCheckerSession = session;
+ if (session.asBinder() instanceof Binder && mThread == null) {
+ if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
+ // If this is a local object, we need to do our own threading
+ // to make sure we handle it asynchronously.
+ mThread = new HandlerThread("SpellCheckerSession",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mThread.start();
+ mAsyncHandler = new Handler(mThread.getLooper()) {
+ @Override public void handleMessage(Message msg) {
+ SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
+ processTask(scp.mSession, scp, true);
+ }
+ };
+ }
+ mState = STATE_CONNECTED;
+ if (DBG) {
+ Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
+ + Integer.toHexString(mISpellCheckerSession.hashCode())
+ + " mPendingTasks.size()=" + mPendingTasks.size());
+ }
+ while (!mPendingTasks.isEmpty()) {
+ processTask(session, mPendingTasks.poll(), false);
+ }
+ }
+ }
+
+ public void cancel() {
+ processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
+ }
+
+ public void getSuggestionsMultiple(
+ TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
+ processOrEnqueueTask(
+ new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
+ suggestionsLimit, sequentialWords));
+ }
+
+ public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
+ processOrEnqueueTask(
+ new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
+ textInfos, suggestionsLimit, false));
+ }
+
+ public void close() {
+ processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
+ }
+
+ public boolean isDisconnected() {
+ synchronized (this) {
+ return mState != STATE_CONNECTED;
+ }
+ }
+
+ private void processOrEnqueueTask(SpellCheckerParams scp) {
+ ISpellCheckerSession session;
+ synchronized (this) {
+ if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
+ Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
+ + taskToString(scp.mWhat)
+ + " scp.mWhat=" + taskToString(scp.mWhat));
+ return;
+ }
+
+ if (mState == STATE_WAIT_CONNECTION) {
+ // If we are still waiting for the connection. Need to pay special attention.
+ if (scp.mWhat == TASK_CLOSE) {
+ processCloseLocked();
+ return;
+ }
+ // Enqueue the task to task queue.
+ SpellCheckerParams closeTask = null;
+ if (scp.mWhat == TASK_CANCEL) {
+ if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
+ while (!mPendingTasks.isEmpty()) {
+ final SpellCheckerParams tmp = mPendingTasks.poll();
+ if (tmp.mWhat == TASK_CLOSE) {
+ // Only one close task should be processed, while we need to remove
+ // all close tasks from the queue
+ closeTask = tmp;
+ }
+ }
+ }
+ mPendingTasks.offer(scp);
+ if (closeTask != null) {
+ mPendingTasks.offer(closeTask);
+ }
+ if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
+ + " connection is not established."
+ + " mPendingTasks.size()=" + mPendingTasks.size());
+ return;
+ }
+
+ session = mISpellCheckerSession;
+ }
+ // session must never be null here.
+ processTask(session, scp, false);
+ }
+
+ @Override
+ public void onGetSuggestions(SuggestionsInfo[] results) {
+ synchronized (this) {
+ if (mHandler != null) {
+ mHandler.sendMessage(Message.obtain(mHandler,
+ MSG_ON_GET_SUGGESTION_MULTIPLE, results));
+ }
+ }
+ }
+
+ @Override
+ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
+ synchronized (this) {
+ if (mHandler != null) {
+ mHandler.sendMessage(Message.obtain(mHandler,
+ MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
+ }
+ }
+ }
+ }
+
+ /**
+ * Callback for getting results from text services
+ */
+ public interface SpellCheckerSessionListener {
+ /**
+ * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
+ * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
+ * @param results an array of {@link SuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s queried by
+ * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
+ * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
+ */
+ public void onGetSuggestions(SuggestionsInfo[] results);
+ /**
+ * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
+ * @param results an array of {@link SentenceSuggestionsInfo}s.
+ * These results are suggestions for {@link TextInfo}s
+ * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
+ */
+ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
+ }
+
+ private static final class InternalListener extends ITextServicesSessionListener.Stub {
+ private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
+
+ public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
+ mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
+ }
+
+ @Override
+ public void onServiceConnected(ISpellCheckerSession session) {
+ mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ // Note that mGuard will be null if the constructor threw.
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ close();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public ITextServicesSessionListener getTextServicesSessionListener() {
+ return mInternalListener;
+ }
+
+ /**
+ * @hide
+ */
+ public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
+ return mSpellCheckerSessionListenerImpl;
+ }
+}
diff --git a/android/view/textservice/SpellCheckerSubtype.java b/android/view/textservice/SpellCheckerSubtype.java
new file mode 100644
index 00000000..026610ec
--- /dev/null
+++ b/android/view/textservice/SpellCheckerSubtype.java
@@ -0,0 +1,321 @@
+/*
+ * 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.view.textservice;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.inputmethod.InputMethodUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * This class is used to specify meta information of a subtype contained in a spell checker.
+ * Subtype can describe locale (e.g. en_US, fr_FR...) used for settings.
+ *
+ * @see SpellCheckerInfo
+ *
+ * @attr ref android.R.styleable#SpellChecker_Subtype_label
+ * @attr ref android.R.styleable#SpellChecker_Subtype_languageTag
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeLocale
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeExtraValue
+ * @attr ref android.R.styleable#SpellChecker_Subtype_subtypeId
+ */
+public final class SpellCheckerSubtype implements Parcelable {
+ private static final String TAG = SpellCheckerSubtype.class.getSimpleName();
+ private static final String EXTRA_VALUE_PAIR_SEPARATOR = ",";
+ private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "=";
+ /**
+ * @hide
+ */
+ public static final int SUBTYPE_ID_NONE = 0;
+ private static final String SUBTYPE_LANGUAGE_TAG_NONE = "";
+
+ private final int mSubtypeId;
+ private final int mSubtypeHashCode;
+ private final int mSubtypeNameResId;
+ private final String mSubtypeLocale;
+ private final String mSubtypeLanguageTag;
+ private final String mSubtypeExtraValue;
+ private HashMap<String, String> mExtraValueHashMapCache;
+
+ /**
+ * Constructor.
+ *
+ * <p>There is no public API that requires developers to instantiate custom
+ * {@link SpellCheckerSubtype} object. Hence so far there is no need to make this constructor
+ * available in public API.</p>
+ *
+ * @param nameId The name of the subtype
+ * @param locale The locale supported by the subtype
+ * @param languageTag The BCP-47 Language Tag associated with this subtype.
+ * @param extraValue The extra value of the subtype
+ * @param subtypeId The subtype ID that is supposed to be stable during package update.
+ *
+ * @hide
+ */
+ public SpellCheckerSubtype(int nameId, String locale, String languageTag, String extraValue,
+ int subtypeId) {
+ mSubtypeNameResId = nameId;
+ mSubtypeLocale = locale != null ? locale : "";
+ mSubtypeLanguageTag = languageTag != null ? languageTag : SUBTYPE_LANGUAGE_TAG_NONE;
+ mSubtypeExtraValue = extraValue != null ? extraValue : "";
+ mSubtypeId = subtypeId;
+ mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+ mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+ }
+
+ /**
+ * Constructor.
+ * @param nameId The name of the subtype
+ * @param locale The locale supported by the subtype
+ * @param extraValue The extra value of the subtype
+ *
+ * @deprecated There is no public API that requires developers to directly instantiate custom
+ * {@link SpellCheckerSubtype} objects right now. Hence only the system is expected to be able
+ * to instantiate {@link SpellCheckerSubtype} object.
+ */
+ @Deprecated
+ public SpellCheckerSubtype(int nameId, String locale, String extraValue) {
+ this(nameId, locale, SUBTYPE_LANGUAGE_TAG_NONE, extraValue, SUBTYPE_ID_NONE);
+ }
+
+ SpellCheckerSubtype(Parcel source) {
+ String s;
+ mSubtypeNameResId = source.readInt();
+ s = source.readString();
+ mSubtypeLocale = s != null ? s : "";
+ s = source.readString();
+ mSubtypeLanguageTag = s != null ? s : "";
+ s = source.readString();
+ mSubtypeExtraValue = s != null ? s : "";
+ mSubtypeId = source.readInt();
+ mSubtypeHashCode = mSubtypeId != SUBTYPE_ID_NONE ?
+ mSubtypeId : hashCodeInternal(mSubtypeLocale, mSubtypeExtraValue);
+ }
+
+ /**
+ * @return the name of the subtype
+ */
+ public int getNameResId() {
+ return mSubtypeNameResId;
+ }
+
+ /**
+ * @return the locale of the subtype
+ *
+ * @deprecated Use {@link #getLanguageTag()} instead.
+ */
+ @Deprecated
+ @NonNull
+ public String getLocale() {
+ return mSubtypeLocale;
+ }
+
+ /**
+ * @return the BCP-47 Language Tag of the subtype. Returns an empty string when no Language Tag
+ * is specified.
+ *
+ * @see Locale#forLanguageTag(String)
+ */
+ @NonNull
+ public String getLanguageTag() {
+ return mSubtypeLanguageTag;
+ }
+
+ /**
+ * @return the extra value of the subtype
+ */
+ public String getExtraValue() {
+ return mSubtypeExtraValue;
+ }
+
+ private HashMap<String, String> getExtraValueHashMap() {
+ if (mExtraValueHashMapCache == null) {
+ mExtraValueHashMapCache = new HashMap<String, String>();
+ final String[] pairs = mSubtypeExtraValue.split(EXTRA_VALUE_PAIR_SEPARATOR);
+ final int N = pairs.length;
+ for (int i = 0; i < N; ++i) {
+ final String[] pair = pairs[i].split(EXTRA_VALUE_KEY_VALUE_SEPARATOR);
+ if (pair.length == 1) {
+ mExtraValueHashMapCache.put(pair[0], null);
+ } else if (pair.length > 1) {
+ if (pair.length > 2) {
+ Slog.w(TAG, "ExtraValue has two or more '='s");
+ }
+ mExtraValueHashMapCache.put(pair[0], pair[1]);
+ }
+ }
+ }
+ return mExtraValueHashMapCache;
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the subtype contains specified the extra value
+ */
+ public boolean containsExtraValueKey(String key) {
+ return getExtraValueHashMap().containsKey(key);
+ }
+
+ /**
+ * The string of ExtraValue in subtype should be defined as follows:
+ * example: key0,key1=value1,key2,key3,key4=value4
+ * @param key the key of extra value
+ * @return the value of the specified key
+ */
+ public String getExtraValueOf(String key) {
+ return getExtraValueHashMap().get(key);
+ }
+
+ @Override
+ public int hashCode() {
+ return mSubtypeHashCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof SpellCheckerSubtype) {
+ SpellCheckerSubtype subtype = (SpellCheckerSubtype) o;
+ if (subtype.mSubtypeId != SUBTYPE_ID_NONE || mSubtypeId != SUBTYPE_ID_NONE) {
+ return (subtype.hashCode() == hashCode());
+ }
+ return (subtype.hashCode() == hashCode())
+ && (subtype.getNameResId() == getNameResId())
+ && (subtype.getLocale().equals(getLocale()))
+ && (subtype.getLanguageTag().equals(getLanguageTag()))
+ && (subtype.getExtraValue().equals(getExtraValue()));
+ }
+ return false;
+ }
+
+ /**
+ * @return {@link Locale} constructed from {@link #getLanguageTag()}. If the Language Tag is not
+ * specified, then try to construct from {@link #getLocale()}
+ *
+ * <p>TODO: Consider to make this a public API, or move this to support lib.</p>
+ * @hide
+ */
+ @Nullable
+ public Locale getLocaleObject() {
+ if (!TextUtils.isEmpty(mSubtypeLanguageTag)) {
+ return Locale.forLanguageTag(mSubtypeLanguageTag);
+ }
+ return InputMethodUtils.constructLocaleFromString(mSubtypeLocale);
+ }
+
+ /**
+ * @param context Context will be used for getting Locale and PackageManager.
+ * @param packageName The package name of the spell checker
+ * @param appInfo The application info of the spell checker
+ * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId)
+ * can have only one %s in it. If there is, the %s part will be replaced with the locale's
+ * display name by the formatter. If there is not, this method simply returns the string
+ * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the
+ * framework to generate an appropriate display name.
+ */
+ public CharSequence getDisplayName(
+ Context context, String packageName, ApplicationInfo appInfo) {
+ final Locale locale = getLocaleObject();
+ final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale;
+ if (mSubtypeNameResId == 0) {
+ return localeStr;
+ }
+ final CharSequence subtypeName = context.getPackageManager().getText(
+ packageName, mSubtypeNameResId, appInfo);
+ if (!TextUtils.isEmpty(subtypeName)) {
+ return String.format(subtypeName.toString(), localeStr);
+ } else {
+ return localeStr;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ dest.writeInt(mSubtypeNameResId);
+ dest.writeString(mSubtypeLocale);
+ dest.writeString(mSubtypeLanguageTag);
+ dest.writeString(mSubtypeExtraValue);
+ dest.writeInt(mSubtypeId);
+ }
+
+ public static final Parcelable.Creator<SpellCheckerSubtype> CREATOR
+ = new Parcelable.Creator<SpellCheckerSubtype>() {
+ @Override
+ public SpellCheckerSubtype createFromParcel(Parcel source) {
+ return new SpellCheckerSubtype(source);
+ }
+
+ @Override
+ public SpellCheckerSubtype[] newArray(int size) {
+ return new SpellCheckerSubtype[size];
+ }
+ };
+
+ private static int hashCodeInternal(String locale, String extraValue) {
+ return Arrays.hashCode(new Object[] {locale, extraValue});
+ }
+
+ /**
+ * Sort the list of subtypes
+ * @param context Context will be used for getting localized strings
+ * @param flags Flags for the sort order
+ * @param sci SpellCheckerInfo of which subtypes are subject to be sorted
+ * @param subtypeList List which will be sorted
+ * @return Sorted list of subtypes
+ * @hide
+ */
+ public static List<SpellCheckerSubtype> sort(Context context, int flags, SpellCheckerInfo sci,
+ List<SpellCheckerSubtype> subtypeList) {
+ if (sci == null) return subtypeList;
+ final HashSet<SpellCheckerSubtype> subtypesSet = new HashSet<SpellCheckerSubtype>(
+ subtypeList);
+ final ArrayList<SpellCheckerSubtype> sortedList = new ArrayList<SpellCheckerSubtype>();
+ int N = sci.getSubtypeCount();
+ for (int i = 0; i < N; ++i) {
+ SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+ if (subtypesSet.contains(subtype)) {
+ sortedList.add(subtype);
+ subtypesSet.remove(subtype);
+ }
+ }
+ // If subtypes in subtypesSet remain, that means these subtypes are not
+ // contained in sci, so the remaining subtypes will be appended.
+ for (SpellCheckerSubtype subtype: subtypesSet) {
+ sortedList.add(subtype);
+ }
+ return sortedList;
+ }
+}
diff --git a/android/view/textservice/SuggestionsInfo.java b/android/view/textservice/SuggestionsInfo.java
new file mode 100644
index 00000000..dc2051cc
--- /dev/null
+++ b/android/view/textservice/SuggestionsInfo.java
@@ -0,0 +1,185 @@
+/*
+ * 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.view.textservice;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.ArrayUtils;
+
+/**
+ * This class contains a metadata of suggestions from the text service
+ */
+public final class SuggestionsInfo implements Parcelable {
+ private static final String[] EMPTY = ArrayUtils.emptyArray(String.class);
+
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the requested word was found
+ * in the dictionary in the text service.
+ */
+ public static final int RESULT_ATTR_IN_THE_DICTIONARY = 0x0001;
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service thinks the requested
+ * word looks like a typo.
+ */
+ public static final int RESULT_ATTR_LOOKS_LIKE_TYPO = 0x0002;
+ /**
+ * Flag of the attributes of the suggestions that can be obtained by
+ * {@link #getSuggestionsAttributes}: this tells that the text service thinks
+ * the result suggestions include highly recommended ones.
+ */
+ public static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = 0x0004;
+ private final int mSuggestionsAttributes;
+ private final String[] mSuggestions;
+ private final boolean mSuggestionsAvailable;
+ private int mCookie;
+ private int mSequence;
+
+ /**
+ * Constructor.
+ * @param suggestionsAttributes from the text service
+ * @param suggestions from the text service
+ */
+ public SuggestionsInfo(int suggestionsAttributes, String[] suggestions) {
+ this(suggestionsAttributes, suggestions, 0, 0);
+ }
+
+ /**
+ * Constructor.
+ * @param suggestionsAttributes from the text service
+ * @param suggestions from the text service
+ * @param cookie the cookie of the input TextInfo
+ * @param sequence the cookie of the input TextInfo
+ */
+ public SuggestionsInfo(
+ int suggestionsAttributes, String[] suggestions, int cookie, int sequence) {
+ if (suggestions == null) {
+ mSuggestions = EMPTY;
+ mSuggestionsAvailable = false;
+ } else {
+ mSuggestions = suggestions;
+ mSuggestionsAvailable = true;
+ }
+ mSuggestionsAttributes = suggestionsAttributes;
+ mCookie = cookie;
+ mSequence = sequence;
+ }
+
+ public SuggestionsInfo(Parcel source) {
+ mSuggestionsAttributes = source.readInt();
+ mSuggestions = source.readStringArray();
+ mCookie = source.readInt();
+ mSequence = source.readInt();
+ mSuggestionsAvailable = source.readInt() == 1;
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSuggestionsAttributes);
+ dest.writeStringArray(mSuggestions);
+ dest.writeInt(mCookie);
+ dest.writeInt(mSequence);
+ dest.writeInt(mSuggestionsAvailable ? 1 : 0);
+ }
+
+ /**
+ * Set the cookie and the sequence of SuggestionsInfo which are set to TextInfo from a client
+ * application
+ * @param cookie the cookie of an input TextInfo
+ * @param sequence the cookie of an input TextInfo
+ */
+ public void setCookieAndSequence(int cookie, int sequence) {
+ mCookie = cookie;
+ mSequence = sequence;
+ }
+
+ /**
+ * @return the cookie which may be set by a client application
+ */
+ public int getCookie() {
+ return mCookie;
+ }
+
+ /**
+ * @return the sequence which may be set by a client application
+ */
+ public int getSequence() {
+ return mSequence;
+ }
+
+ /**
+ * @return the attributes of suggestions. This includes whether the spell checker has the word
+ * in its dictionary or not and whether the spell checker has confident suggestions for the
+ * word or not.
+ */
+ public int getSuggestionsAttributes() {
+ return mSuggestionsAttributes;
+ }
+
+ /**
+ * @return the count of the suggestions. If there's no suggestions at all, this method returns
+ * -1. Even if this method returns 0, it doesn't necessarily mean that there are no suggestions
+ * for the requested word. For instance, the caller could have been asked to limit the maximum
+ * number of suggestions returned.
+ */
+ public int getSuggestionsCount() {
+ if (!mSuggestionsAvailable) {
+ return -1;
+ }
+ return mSuggestions.length;
+ }
+
+ /**
+ * @param i the id of suggestions
+ * @return the suggestion at the specified id
+ */
+ public String getSuggestionAt(int i) {
+ return mSuggestions[i];
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<SuggestionsInfo> CREATOR
+ = new Parcelable.Creator<SuggestionsInfo>() {
+ @Override
+ public SuggestionsInfo createFromParcel(Parcel source) {
+ return new SuggestionsInfo(source);
+ }
+
+ @Override
+ public SuggestionsInfo[] newArray(int size) {
+ return new SuggestionsInfo[size];
+ }
+ };
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/textservice/TextInfo.java b/android/view/textservice/TextInfo.java
new file mode 100644
index 00000000..5499918a
--- /dev/null
+++ b/android/view/textservice/TextInfo.java
@@ -0,0 +1,161 @@
+/*
+ * 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.view.textservice;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.ParcelableSpan;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.SpellCheckSpan;
+
+/**
+ * This class contains a metadata of the input of TextService
+ */
+public final class TextInfo implements Parcelable {
+ private final CharSequence mCharSequence;
+ private final int mCookie;
+ private final int mSequenceNumber;
+
+ private static final int DEFAULT_COOKIE = 0;
+ private static final int DEFAULT_SEQUENCE_NUMBER = 0;
+
+ /**
+ * Constructor.
+ * @param text the text which will be input to TextService
+ */
+ public TextInfo(String text) {
+ this(text, 0, getStringLengthOrZero(text), DEFAULT_COOKIE, DEFAULT_SEQUENCE_NUMBER);
+ }
+
+ /**
+ * Constructor.
+ * @param text the text which will be input to TextService
+ * @param cookie the cookie for this TextInfo
+ * @param sequenceNumber the sequence number for this TextInfo
+ */
+ public TextInfo(String text, int cookie, int sequenceNumber) {
+ this(text, 0, getStringLengthOrZero(text), cookie, sequenceNumber);
+ }
+
+ private static int getStringLengthOrZero(final String text) {
+ return TextUtils.isEmpty(text) ? 0 : text.length();
+ }
+
+ /**
+ * Constructor.
+ * @param charSequence the text which will be input to TextService. Attached spans that
+ * implement {@link ParcelableSpan} will also be marshaled alongside with the text.
+ * @param start the beginning of the range of text (inclusive).
+ * @param end the end of the range of text (exclusive).
+ * @param cookie the cookie for this TextInfo
+ * @param sequenceNumber the sequence number for this TextInfo
+ */
+ public TextInfo(CharSequence charSequence, int start, int end, int cookie, int sequenceNumber) {
+ if (TextUtils.isEmpty(charSequence)) {
+ throw new IllegalArgumentException("charSequence is empty");
+ }
+ // Create a snapshot of the text including spans in case they are updated outside later.
+ final SpannableStringBuilder spannableString =
+ new SpannableStringBuilder(charSequence, start, end);
+ // SpellCheckSpan is for internal use. We do not want to marshal this for TextService.
+ final SpellCheckSpan[] spans = spannableString.getSpans(0, spannableString.length(),
+ SpellCheckSpan.class);
+ for (int i = 0; i < spans.length; ++i) {
+ spannableString.removeSpan(spans[i]);
+ }
+
+ mCharSequence = spannableString;
+ mCookie = cookie;
+ mSequenceNumber = sequenceNumber;
+ }
+
+ public TextInfo(Parcel source) {
+ mCharSequence = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
+ mCookie = source.readInt();
+ mSequenceNumber = source.readInt();
+ }
+
+ /**
+ * Used to package this object into a {@link Parcel}.
+ *
+ * @param dest The {@link Parcel} to be written.
+ * @param flags The flags used for parceling.
+ */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ TextUtils.writeToParcel(mCharSequence, dest, flags);
+ dest.writeInt(mCookie);
+ dest.writeInt(mSequenceNumber);
+ }
+
+ /**
+ * @return the text which is an input of a text service
+ */
+ public String getText() {
+ if (mCharSequence == null) {
+ return null;
+ }
+ return mCharSequence.toString();
+ }
+
+ /**
+ * @return the charSequence which is an input of a text service. This may have some parcelable
+ * spans.
+ */
+ public CharSequence getCharSequence() {
+ return mCharSequence;
+ }
+
+ /**
+ * @return the cookie of TextInfo
+ */
+ public int getCookie() {
+ return mCookie;
+ }
+
+ /**
+ * @return the sequence of TextInfo
+ */
+ public int getSequence() {
+ return mSequenceNumber;
+ }
+
+ /**
+ * Used to make this class parcelable.
+ */
+ public static final Parcelable.Creator<TextInfo> CREATOR
+ = new Parcelable.Creator<TextInfo>() {
+ @Override
+ public TextInfo createFromParcel(Parcel source) {
+ return new TextInfo(source);
+ }
+
+ @Override
+ public TextInfo[] newArray(int size) {
+ return new TextInfo[size];
+ }
+ };
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java
new file mode 100644
index 00000000..f368c74a
--- /dev/null
+++ b/android/view/textservice/TextServicesManager.java
@@ -0,0 +1,236 @@
+/*
+ * 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.view.textservice;
+
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
+
+import com.android.internal.textservice.ITextServicesManager;
+
+import java.util.Locale;
+
+/**
+ * System API to the overall text services, which arbitrates interaction between applications
+ * and text services.
+ *
+ * The user can change the current text services in Settings. And also applications can specify
+ * the target text services.
+ *
+ * <h3>Architecture Overview</h3>
+ *
+ * <p>There are three primary parties involved in the text services
+ * framework (TSF) architecture:</p>
+ *
+ * <ul>
+ * <li> The <strong>text services manager</strong> as expressed by this class
+ * is the central point of the system that manages interaction between all
+ * other parts. It is expressed as the client-side API here which exists
+ * in each application context and communicates with a global system service
+ * that manages the interaction across all processes.
+ * <li> A <strong>text service</strong> implements a particular
+ * interaction model allowing the client application to retrieve information of text.
+ * The system binds to the current text service that is in use, causing it to be created and run.
+ * <li> Multiple <strong>client applications</strong> arbitrate with the text service
+ * manager for connections to text services.
+ * </ul>
+ *
+ * <h3>Text services sessions</h3>
+ * <ul>
+ * <li>The <strong>spell checker session</strong> is one of the text services.
+ * {@link android.view.textservice.SpellCheckerSession}</li>
+ * </ul>
+ *
+ */
+@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)
+public final class TextServicesManager {
+ private static final String TAG = TextServicesManager.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ private static TextServicesManager sInstance;
+
+ private final ITextServicesManager mService;
+
+ private TextServicesManager() throws ServiceNotFoundException {
+ mService = ITextServicesManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
+ }
+
+ /**
+ * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
+ * @hide
+ */
+ public static TextServicesManager getInstance() {
+ synchronized (TextServicesManager.class) {
+ if (sInstance == null) {
+ try {
+ sInstance = new TextServicesManager();
+ } catch (ServiceNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Returns the language component of a given locale string.
+ */
+ private static String parseLanguageFromLocaleString(String locale) {
+ final int idx = locale.indexOf('_');
+ if (idx < 0) {
+ return locale;
+ } else {
+ return locale.substring(0, idx);
+ }
+ }
+
+ /**
+ * Get a spell checker session for the specified spell checker
+ * @param locale the locale for the spell checker. If {@code locale} is null and
+ * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
+ * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
+ * the locale specified in Settings will be returned only when it is same as {@code locale}.
+ * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
+ * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
+ * selected.
+ * @param listener a spell checker session lister for getting results from a spell checker.
+ * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
+ * languages in settings will be returned.
+ * @return the spell checker session of the spell checker
+ */
+ public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
+ SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
+ if (listener == null) {
+ throw new NullPointerException();
+ }
+ if (!referToSpellCheckerLanguageSettings && locale == null) {
+ throw new IllegalArgumentException("Locale should not be null if you don't refer"
+ + " settings.");
+ }
+
+ if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
+ return null;
+ }
+
+ final SpellCheckerInfo sci;
+ try {
+ sci = mService.getCurrentSpellChecker(null);
+ } catch (RemoteException e) {
+ return null;
+ }
+ if (sci == null) {
+ return null;
+ }
+ SpellCheckerSubtype subtypeInUse = null;
+ if (referToSpellCheckerLanguageSettings) {
+ subtypeInUse = getCurrentSpellCheckerSubtype(true);
+ if (subtypeInUse == null) {
+ return null;
+ }
+ if (locale != null) {
+ final String subtypeLocale = subtypeInUse.getLocale();
+ final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
+ if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) {
+ return null;
+ }
+ }
+ } else {
+ final String localeStr = locale.toString();
+ for (int i = 0; i < sci.getSubtypeCount(); ++i) {
+ final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+ final String tempSubtypeLocale = subtype.getLocale();
+ final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale);
+ if (tempSubtypeLocale.equals(localeStr)) {
+ subtypeInUse = subtype;
+ break;
+ } else if (tempSubtypeLanguage.length() >= 2 &&
+ locale.getLanguage().equals(tempSubtypeLanguage)) {
+ subtypeInUse = subtype;
+ }
+ }
+ }
+ if (subtypeInUse == null) {
+ return null;
+ }
+ final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener);
+ try {
+ mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
+ session.getTextServicesSessionListener(),
+ session.getSpellCheckerSessionListener(), bundle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return session;
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerInfo[] getEnabledSpellCheckers() {
+ try {
+ final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers();
+ if (DBG) {
+ Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
+ }
+ return retval;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerInfo getCurrentSpellChecker() {
+ try {
+ // Passing null as a locale for ICS
+ return mService.getCurrentSpellChecker(null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
+ boolean allowImplicitlySelectedSubtype) {
+ try {
+ // Passing null as a locale until we support multiple enabled spell checker subtypes.
+ return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public boolean isSpellCheckerEnabled() {
+ try {
+ return mService.isSpellCheckerEnabled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}