diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/view/textservice | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-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.java | 144 | ||||
-rw-r--r-- | android/view/textservice/SpellCheckerInfo.java | 290 | ||||
-rw-r--r-- | android/view/textservice/SpellCheckerSession.java | 572 | ||||
-rw-r--r-- | android/view/textservice/SpellCheckerSubtype.java | 321 | ||||
-rw-r--r-- | android/view/textservice/SuggestionsInfo.java | 185 | ||||
-rw-r--r-- | android/view/textservice/TextInfo.java | 161 | ||||
-rw-r--r-- | android/view/textservice/TextServicesManager.java | 236 |
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(); + } + } +} |