summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk3
-rw-r--r--api/current.txt48
-rw-r--r--core/java/android/app/ContextImpl.java6
-rw-r--r--core/java/android/content/Context.java9
-rw-r--r--core/java/android/provider/Settings.java12
-rw-r--r--core/java/android/view/inputmethod/ISpellCheckerService.aidl27
-rw-r--r--core/java/android/view/inputmethod/SpellCheckerInfo.aidl19
-rw-r--r--core/java/android/view/inputmethod/SpellCheckerInfo.java109
-rw-r--r--core/java/android/view/inputmethod/SpellCheckerService.java144
-rw-r--r--core/java/android/view/inputmethod/TextServiceManager.java393
-rw-r--r--core/java/com/android/internal/view/ITextServiceManager.aidl28
-rw-r--r--core/res/AndroidManifest.xml7
-rwxr-xr-xcore/res/res/values/strings.xml6
-rw-r--r--services/java/com/android/server/SystemServer.java11
-rw-r--r--services/java/com/android/server/TextServiceManagerService.java169
15 files changed, 988 insertions, 3 deletions
diff --git a/Android.mk b/Android.mk
index d4d9a33..c9a8213 100644
--- a/Android.mk
+++ b/Android.mk
@@ -136,6 +136,7 @@ LOCAL_SRC_FILES += \
core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl\
core/java/android/view/accessibility/IAccessibilityManager.aidl \
core/java/android/view/accessibility/IAccessibilityManagerClient.aidl \
+ core/java/android/view/inputmethod/ISpellCheckerService.aidl \
core/java/android/view/IApplicationToken.aidl \
core/java/android/view/IOnKeyguardExitResult.aidl \
core/java/android/view/IRotationWatcher.aidl \
@@ -163,6 +164,7 @@ LOCAL_SRC_FILES += \
core/java/com/android/internal/view/IInputMethodClient.aidl \
core/java/com/android/internal/view/IInputMethodManager.aidl \
core/java/com/android/internal/view/IInputMethodSession.aidl \
+ core/java/com/android/internal/view/ITextServiceManager.aidl \
core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \
core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl \
keystore/java/android/security/IKeyChainAliasCallback.aidl \
@@ -270,6 +272,7 @@ aidl_files := \
frameworks/base/core/java/com/android/internal/view/IInputMethodClient.aidl \
frameworks/base/core/java/com/android/internal/view/IInputMethodManager.aidl \
frameworks/base/core/java/com/android/internal/view/IInputMethodSession.aidl \
+ frameworks/base/core/java/com/android/internal/view/ITextServiceManager.aidl \
frameworks/base/graphics/java/android/graphics/Bitmap.aidl \
frameworks/base/graphics/java/android/graphics/Rect.aidl \
frameworks/base/graphics/java/android/graphics/Region.aidl \
diff --git a/api/current.txt b/api/current.txt
index 721e33e..a499196 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -21,6 +21,7 @@ package android {
field public static final java.lang.String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD";
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
+ field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE";
field public static final java.lang.String BIND_WALLPAPER = "android.permission.BIND_WALLPAPER";
field public static final java.lang.String BLUETOOTH = "android.permission.BLUETOOTH";
field public static final java.lang.String BLUETOOTH_ADMIN = "android.permission.BLUETOOTH_ADMIN";
@@ -4733,6 +4734,7 @@ package android.content {
field public static final java.lang.String SENSOR_SERVICE = "sensor";
field public static final java.lang.String STORAGE_SERVICE = "storage";
field public static final java.lang.String TELEPHONY_SERVICE = "phone";
+ field public static final java.lang.String TEXT_SERVICE_MANAGER_SERVICE = "text_service_manager_service";
field public static final java.lang.String UI_MODE_SERVICE = "uimode";
field public static final java.lang.String USB_SERVICE = "usb";
field public static final java.lang.String VIBRATOR_SERVICE = "vibrator";
@@ -23336,6 +23338,19 @@ package android.view.inputmethod {
field public int token;
}
+ public abstract interface ISpellCheckerService implements android.os.IInterface {
+ method public abstract void cancel() throws android.os.RemoteException;
+ method public abstract java.lang.CharSequence getSuggestions(java.lang.CharSequence, int, int, java.lang.String) throws android.os.RemoteException;
+ method public abstract boolean isCorrect(java.lang.CharSequence, int, int, java.lang.String) throws android.os.RemoteException;
+ }
+
+ public static abstract class ISpellCheckerService.Stub extends android.os.Binder implements android.view.inputmethod.ISpellCheckerService {
+ ctor public ISpellCheckerService.Stub();
+ method public android.os.IBinder asBinder();
+ method public static android.view.inputmethod.ISpellCheckerService asInterface(android.os.IBinder);
+ method public boolean onTransact(int, android.os.Parcel, android.os.Parcel, int) throws android.os.RemoteException;
+ }
+
public final class InputBinding implements android.os.Parcelable {
ctor public InputBinding(android.view.inputmethod.InputConnection, android.os.IBinder, int, int);
ctor public InputBinding(android.view.inputmethod.InputConnection, android.view.inputmethod.InputBinding);
@@ -23520,6 +23535,39 @@ package android.view.inputmethod {
field public static final android.os.Parcelable.Creator CREATOR;
}
+ public final class SpellCheckerInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.content.ComponentName getComponent();
+ method public java.lang.String getId();
+ method public java.lang.String getPackageName();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ }
+
+ public abstract class SpellCheckerService extends android.app.Service {
+ ctor public SpellCheckerService();
+ method protected void cancel();
+ method protected static java.util.Locale constructLocaleFromString(java.lang.String);
+ method protected abstract java.lang.String[] getStringSuggestions(java.lang.CharSequence, int, int, java.lang.String);
+ method protected java.lang.CharSequence getSuggestions(java.lang.CharSequence, int, int, java.lang.String);
+ method protected abstract boolean isCorrect(java.lang.CharSequence, int, int, java.lang.String);
+ method public final android.os.IBinder onBind(android.content.Intent);
+ field public static final java.lang.String SERVICE_INTERFACE;
+ }
+
+ public final class TextServiceManager {
+ method public void getSuggestions(java.lang.CharSequence, int, int, java.util.Locale, boolean, android.view.inputmethod.TextServiceManager.Callback);
+ method public void isCorrect(java.lang.CharSequence, android.view.inputmethod.TextServiceManager.Callback);
+ method public void isCorrect(java.lang.CharSequence, java.util.Locale, android.view.inputmethod.TextServiceManager.Callback);
+ method public void isCorrect(java.lang.CharSequence, int, int, java.util.Locale, android.view.inputmethod.TextServiceManager.Callback);
+ method public android.view.inputmethod.SpellCheckerInfo requestSpellCheckerConnection(java.util.Locale);
+ }
+
+ public static abstract interface TextServiceManager.Callback {
+ method public abstract void getSuggestionsResult(java.lang.CharSequence, int, int, java.util.Locale, java.lang.CharSequence);
+ method public abstract void isCorrectResult(java.lang.CharSequence, int, int, java.util.Locale, boolean);
+ }
+
}
package android.webkit {
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 94a4afa..09b7dd4 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -85,6 +85,7 @@ import android.view.Display;
import android.view.WindowManagerImpl;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.TextServiceManager;
import android.accounts.AccountManager;
import android.accounts.IAccountManager;
import android.app.admin.DevicePolicyManager;
@@ -321,6 +322,11 @@ class ContextImpl extends Context {
return InputMethodManager.getInstance(ctx);
}});
+ registerService(TEXT_SERVICE_MANAGER_SERVICE, new ServiceFetcher() {
+ public Object createService(ContextImpl ctx) {
+ return TextServiceManager.getInstance(ctx);
+ }});
+
registerService(KEYGUARD_SERVICE, new ServiceFetcher() {
public Object getService(ContextImpl ctx) {
// TODO: why isn't this caching it? It wasn't
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index aecec66..7261c84 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -1600,6 +1600,15 @@ public abstract class Context {
/**
* Use with {@link #getSystemService} to retrieve a
+ * {@link android.view.inputmethod.TextServiceManager} for accessing
+ * text services.
+ *
+ * @see #getSystemService
+ */
+ public static final String TEXT_SERVICE_MANAGER_SERVICE = "text_service_manager_service";
+
+ /**
+ * Use with {@link #getSystemService} to retrieve a
* {@link android.appwidget.AppWidgetManager} for accessing AppWidgets.
*
* @hide
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 19e9a67..d456813 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -16,8 +16,6 @@
package android.provider;
-
-
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.content.ComponentName;
@@ -47,7 +45,6 @@ import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
-
/**
* The Settings provider contains global system-level device preferences.
*/
@@ -3633,6 +3630,15 @@ public final class Settings {
*/
public static final String VOICE_RECOGNITION_SERVICE = "voice_recognition_service";
+
+ /**
+ * The {@link ComponentName} string of the service to be used as the spell checker
+ * service which is one of the services managed by the text service manager.
+ *
+ * @hide
+ */
+ public static final String SPELL_CHECKER_SERVICE = "spell_checker_service";
+
/**
* What happens when the user presses the Power button while in-call
* and the screen is on.<br/>
diff --git a/core/java/android/view/inputmethod/ISpellCheckerService.aidl b/core/java/android/view/inputmethod/ISpellCheckerService.aidl
new file mode 100644
index 0000000..68e406a
--- /dev/null
+++ b/core/java/android/view/inputmethod/ISpellCheckerService.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.inputmethod;
+
+/**
+ * Public interface to the global input text manager, used by all client
+ * applications.
+ */
+interface ISpellCheckerService {
+ boolean isCorrect(CharSequence text, int start, int end, String locale);
+ CharSequence getSuggestions(CharSequence text, int start, int end, String locale);
+ void cancel();
+}
diff --git a/core/java/android/view/inputmethod/SpellCheckerInfo.aidl b/core/java/android/view/inputmethod/SpellCheckerInfo.aidl
new file mode 100644
index 0000000..0266038
--- /dev/null
+++ b/core/java/android/view/inputmethod/SpellCheckerInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.inputmethod;
+
+parcelable SpellCheckerInfo;
diff --git a/core/java/android/view/inputmethod/SpellCheckerInfo.java b/core/java/android/view/inputmethod/SpellCheckerInfo.java
new file mode 100644
index 0000000..9754df2
--- /dev/null
+++ b/core/java/android/view/inputmethod/SpellCheckerInfo.java
@@ -0,0 +1,109 @@
+/*
+ * 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.inputmethod;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public final class SpellCheckerInfo implements Parcelable {
+ private final ResolveInfo mService;
+ private final String mId;
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Context context, ResolveInfo service) {
+ mService = service;
+ ServiceInfo si = service.serviceInfo;
+ mId = new ComponentName(si.packageName, si.name).flattenToShortString();
+ }
+
+ /**
+ * Constructor.
+ * @hide
+ */
+ public SpellCheckerInfo(Parcel source) {
+ mId = source.readString();
+ mService = ResolveInfo.CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * 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 input method.
+ */
+ 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.writeString(mId);
+ mService.writeToParcel(dest, flags);
+ }
+
+
+ /**
+ * 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];
+ }
+ };
+
+ /**
+ * Used to make this class parcelable.
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/java/android/view/inputmethod/SpellCheckerService.java b/core/java/android/view/inputmethod/SpellCheckerService.java
new file mode 100644
index 0000000..ebd42e3
--- /dev/null
+++ b/core/java/android/view/inputmethod/SpellCheckerService.java
@@ -0,0 +1,144 @@
+/*
+ * 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.inputmethod;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+public abstract class SpellCheckerService extends Service {
+ public static final String SERVICE_INTERFACE = SpellCheckerService.class.getName();
+
+ private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this);
+
+ /**
+ * Check if the substring of text from start to end is a correct word or not in the specified
+ * locale.
+ * @param text the substring of text from start to end will be checked.
+ * @param start the start position of the text to be checked (inclusive)
+ * @param end the end position of the text to be checked (exclusive)
+ * @param locale the locale for checking the text
+ * @return true if the substring of text from start to end is a correct word
+ */
+ protected abstract boolean isCorrect(CharSequence text, int start, int end, String locale);
+
+ /**
+ * @param text the substring of text from start to end for getting suggestions
+ * @param start the start position of the text (inclusive)
+ * @param end the end position of the text (exclusive)
+ * @param locale the locale for getting suggestions
+ * @return text with SuggestionSpan containing suggestions
+ */
+ protected CharSequence getSuggestions(CharSequence text, int start, int end, String locale) {
+ if (TextUtils.isEmpty(text) || TextUtils.isEmpty(locale) || end <= start) {
+ return text;
+ }
+ final String[] suggestions = getStringSuggestions(text, start, end, locale);
+ if (suggestions == null || suggestions.length == 0) {
+ return text;
+ }
+ final Spannable spannable;
+ if (text instanceof Spannable) {
+ spannable = (Spannable) text;
+ } else {
+ spannable = new SpannableString(text);
+ }
+ final int N = Math.min(SuggestionSpan.SUGGESTIONS_MAX_SIZE, suggestions.length);
+ final SuggestionSpan ss = new SuggestionSpan(
+ constructLocaleFromString(locale), Arrays.copyOfRange(suggestions, 0, N), 0);
+ spannable.setSpan(ss, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return spannable;
+ }
+
+ /**
+ * Basic implementation for getting suggestions. This function is called from getSuggestions
+ * and the returned strings array will be set as a SuggestionSpan.
+ * If you want to set SuggestionSpan by yourself, make getStringSuggestions an empty
+ * implementation and override getSuggestions.
+ * @param text the substring of text from start to end for getting suggestions
+ * @param start the start position of the text (inclusive)
+ * @param end the end position of the text (exclusive)
+ * @param locale the locale for getting suggestions
+ * @return strings array for the substring of the specified text.
+ */
+ protected abstract String[] getStringSuggestions(
+ CharSequence text, int start, int end, String locale);
+
+ /**
+ * Request to abort all tasks executed in SpellChecker
+ */
+ protected void cancel() {}
+
+ @Override
+ public final IBinder onBind(final Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ mBinder.clearReference();
+ super.onDestroy();
+ }
+
+ protected static final Locale constructLocaleFromString(String localeStr) {
+ if (TextUtils.isEmpty(localeStr))
+ return null;
+ String[] localeParams = localeStr.split("_", 3);
+ // The length of localeParams is guaranteed to always return a 1 <= value <= 3.
+ if (localeParams.length == 1) {
+ return new Locale(localeParams[0]);
+ } else if (localeParams.length == 2) {
+ return new Locale(localeParams[0], localeParams[1]);
+ } else if (localeParams.length == 3) {
+ return new Locale(localeParams[0], localeParams[1], localeParams[2]);
+ }
+ return null;
+ }
+
+ private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub {
+ private SpellCheckerService mInternalService;
+
+ public SpellCheckerServiceBinder(SpellCheckerService service) {
+ mInternalService = service;
+ }
+
+ @Override
+ public CharSequence getSuggestions(CharSequence text, int start, int end, String locale) {
+ return mInternalService.getSuggestions(text, start, end, locale);
+ }
+
+ @Override
+ public boolean isCorrect(CharSequence text, int start, int end, String locale) {
+ return mInternalService.isCorrect(text, start, end, locale);
+ }
+
+ @Override
+ public void cancel() {}
+
+ private void clearReference() {
+ mInternalService = null;
+ }
+ }
+}
diff --git a/core/java/android/view/inputmethod/TextServiceManager.java b/core/java/android/view/inputmethod/TextServiceManager.java
new file mode 100644
index 0000000..9eecc9d
--- /dev/null
+++ b/core/java/android/view/inputmethod/TextServiceManager.java
@@ -0,0 +1,393 @@
+/*
+ * 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.inputmethod;
+
+import com.android.internal.view.ITextServiceManager;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.inputmethod.ISpellCheckerService;
+import android.view.inputmethod.SpellCheckerService;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Queue;
+
+public final class TextServiceManager {
+ private static final String TAG = TextServiceManager.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final int MSG_CANCEL = 1;
+ private static final int MSG_IS_CORRECT = 2;
+ private static final int MSG_GET_SUGGESTION = 3;
+
+ private static TextServiceManager sInstance;
+ private static ITextServiceManager sService;
+
+ private final WeakReference<Context> mContextRef;
+ private static final HashMap<String, SpellCheckerConnection> sComponentMap =
+ new HashMap<String, SpellCheckerConnection>();
+
+ private TextServiceManager(Context context) {
+ mContextRef = new WeakReference<Context>(context);
+ synchronized (sComponentMap) {
+ if (sService == null) {
+ IBinder b = ServiceManager.getService(Context.TEXT_SERVICE_MANAGER_SERVICE);
+ sService = ITextServiceManager.Stub.asInterface(b);
+ }
+ }
+ }
+
+ /**
+ * Retrieve the global TextServiceManager instance, creating it if it doesn't already exist.
+ * @hide
+ */
+ public static TextServiceManager getInstance(Context context) {
+ synchronized (sComponentMap) {
+ if (sInstance != null) {
+ return sInstance;
+ }
+ sInstance = new TextServiceManager(context);
+ }
+ return sInstance;
+ }
+
+ private static class SpellCheckerConnection implements ServiceConnection {
+ private final String mLocale;
+ private ISpellCheckerService mSpellCheckerService;
+ private final Queue<Message> mPendingTasks = new LinkedList<Message>();
+ private final SpellCheckerInfo mSpellCheckerInfo;
+
+ private static class SpellCheckerParams {
+ public final CharSequence mText;
+ public final int mStart;
+ public final int mEnd;
+ public final Locale mLocale;
+ public final Callback mCallback;
+ public SpellCheckerParams(
+ CharSequence text, int start, int end, Locale locale, Callback callback) {
+ mText = text;
+ mStart = start;
+ mEnd = end;
+ mLocale = locale;
+ mCallback = callback;
+ }
+ }
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CANCEL:
+ handleCancelMessage();
+ break;
+ case MSG_IS_CORRECT:
+ handleIsCorrectMessage((SpellCheckerParams) msg.obj);
+ break;
+ case MSG_GET_SUGGESTION:
+ handleGetSuggestionMessage((SpellCheckerParams) msg.obj);
+ break;
+ }
+ }
+ };
+
+ public SpellCheckerConnection(String locale, SpellCheckerInfo sci) {
+ mLocale = locale;
+ mSpellCheckerInfo = sci;
+ }
+
+ @Override
+ public synchronized void onServiceConnected(
+ final ComponentName name, final IBinder service) {
+ mSpellCheckerService = ISpellCheckerService.Stub.asInterface(service);
+ if (DBG)
+ Log.d(TAG, "onServiceConnected - Success");
+ while (!mPendingTasks.isEmpty()) {
+ mHandler.sendMessage(mPendingTasks.poll());
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(final ComponentName name) {
+ mSpellCheckerService = null;
+ mPendingTasks.clear();
+ synchronized(sComponentMap) {
+ sComponentMap.remove(mLocale);
+ }
+ if (DBG)
+ Log.d(TAG, "onServiceDisconnected - Success");
+ }
+
+ public void isCorrect(
+ CharSequence text, int start, int end, Locale locale, Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("isCorrect: Callback is null.");
+ }
+ putMessage(Message.obtain(mHandler, MSG_IS_CORRECT,
+ new SpellCheckerParams(text, start, end, locale, callback)));
+ }
+
+ public void getSuggestions(
+ CharSequence text, int start, int end, Locale locale, Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("getSuggestions: Callback is null.");
+ }
+ putMessage(Message.obtain(mHandler, MSG_GET_SUGGESTION,
+ new SpellCheckerParams(text, start, end, locale, callback)));
+ }
+
+ public SpellCheckerInfo getSpellCheckerInfo() {
+ return mSpellCheckerInfo;
+ }
+
+ private boolean checkOpenConnection() {
+ if (mSpellCheckerService != null) {
+ return true;
+ }
+ Log.e(TAG, "not connected to the spellchecker service.");
+ return false;
+ }
+
+ private void putMessage(Message msg) {
+ if (mSpellCheckerService == null) {
+ mPendingTasks.offer(msg);
+ } else {
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ private void handleCancelMessage() {
+ if (!checkOpenConnection()) {
+ return;
+ }
+ try {
+ mSpellCheckerService.cancel();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception in cancel.");
+ }
+ }
+
+ private void handleIsCorrectMessage(SpellCheckerParams scp) {
+ if (!checkOpenConnection()) {
+ return;
+ }
+ try {
+ scp.mCallback.isCorrectResult(scp.mText, scp.mStart, scp.mEnd, scp.mLocale,
+ mSpellCheckerService.isCorrect(
+ scp.mText, scp.mStart, scp.mEnd, scp.mLocale.toString()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception in isCorrect.");
+ }
+ }
+
+ private void handleGetSuggestionMessage(SpellCheckerParams scp) {
+ if (!checkOpenConnection()) {
+ return;
+ }
+ try {
+ scp.mCallback.getSuggestionsResult(scp.mText, scp.mStart, scp.mEnd, scp.mLocale,
+ mSpellCheckerService.getSuggestions(
+ scp.mText, scp.mStart, scp.mEnd, scp.mLocale.toString()));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception in getSuggestion.");
+ }
+ }
+ }
+
+ /**
+ * Check if the substring of text from start to end is a correct word or not in the
+ * default locale for the context.
+ * @param text text
+ * @param callback callback for getting the result from SpellChecker
+ * @return true if the substring of text from start to end is a correct word
+ */
+ public void isCorrect(CharSequence text, Callback callback) {
+ final Context context = mContextRef.get();
+ if (context == null) {
+ return;
+ }
+ isCorrect(text, mContextRef.get().getResources().getConfiguration().locale, callback);
+ }
+
+ /**
+ * Check if the substring of text from start to end is a correct word or not in the
+ * specified locale.
+ * @param text text
+ * @param locale the locale for checking the text
+ * @param callback callback for getting the result from SpellChecker
+ * @return true if the substring of text from start to end is a correct word
+ */
+ public void isCorrect(CharSequence text, Locale locale, Callback callback) {
+ isCorrect(text, 0, text.length(), locale, callback);
+ }
+
+ /**
+ * Check if the substring of text from start to end is a correct word or not in the
+ * specified locale.
+ * @param text text
+ * @param start the start position of the text to be checked
+ * @param end the end position of the text to be checked
+ * @param locale the locale for checking the text
+ * @param callback callback for getting the result from SpellChecker
+ * @return true if the substring of text from start to end is a correct word
+ */
+ public void isCorrect(CharSequence text, int start, int end, Locale locale, Callback callback) {
+ if (TextUtils.isEmpty(text) || locale == null || callback == null) {
+ throw new IllegalArgumentException(
+ "text = " + text + ", locale = " + locale + ", callback = " + callback);
+ }
+ final int textSize = text.length();
+ if (start < 0 || textSize <= start || end < 0 || textSize <= end || start >= end) {
+ throw new IndexOutOfBoundsException(
+ "text = " + text + ", start = " + start + ", end = " + end);
+ }
+ final SpellCheckerConnection spellCheckerConnection =
+ getCurrentSpellCheckerConnection(locale, false);
+ if (spellCheckerConnection == null) {
+ Log.e(TAG, "Could not find spellchecker for " + locale);
+ return;
+ }
+ spellCheckerConnection.isCorrect(text, start, end, locale, callback);
+ }
+
+ /**
+ * Get candidate strings for a substring of the specified text.
+ * @param text the substring of text from start to end for getting suggestions
+ * @param start the start position of the text
+ * @param end the end position of the text
+ * @param locale the locale for getting suggestions
+ * @param callback callback for getting the result from SpellChecker
+ * @return text with SuggestionSpan containing suggestions
+ */
+ public void getSuggestions(CharSequence text, int start, int end, Locale locale,
+ boolean allowMultipleWords, Callback callback) {
+ if (TextUtils.isEmpty(text) || locale == null || callback == null) {
+ throw new IllegalArgumentException(
+ "text = " + text + ", locale = " + locale + ", callback = " + callback);
+ }
+ final int textService = text.length();
+ if (start < 0 || textService <= start || end < 0 || textService <= end || start >= end) {
+ throw new IndexOutOfBoundsException(
+ "text = " + text + ", start = " + start + ", end = " + end);
+ }
+ final SpellCheckerConnection spellCheckerConnection = getCurrentSpellCheckerConnection(
+ locale, false);
+ if (spellCheckerConnection == null) {
+ Log.e(TAG, "Could not find spellchecker for " + locale);
+ return;
+ }
+ // TODO: Handle multiple words suggestions by using WordBreakIterator
+ spellCheckerConnection.getSuggestions(text, start, end, locale, callback);
+ }
+
+ /**
+ * Get the current spell checker service for the specified locale. It's recommended
+ * to call this method before calling other APIs in TextServiceManager.
+ * This method may update the current spell checker in use for the specified locale if the user
+ * has selected a different spell checker for the locale.
+ * @param locale locale of a spell checker
+ * @return SpellCheckerInfo for the specified locale.
+ */
+ // TODO: Add a method to get enabled spell checkers.
+ // TODO: Add a method to set a spell checker
+ public SpellCheckerInfo requestSpellCheckerConnection(Locale locale) {
+ if (locale == null) return null;
+ final SpellCheckerConnection scc = getCurrentSpellCheckerConnection(locale, true);
+ if (scc == null) return null;
+ return scc.getSpellCheckerInfo();
+ }
+
+ private SpellCheckerConnection getCurrentSpellCheckerConnection(
+ Locale locale, boolean resetIfChanged) {
+ final Context context = mContextRef.get();
+ if (locale == null) {
+ return null;
+ }
+ if (context == null) {
+ throw new RuntimeException("Context was GCed.");
+ }
+ final String localeStr = locale.toString();
+ SpellCheckerConnection connection = null;
+ synchronized (sComponentMap) {
+ if (sComponentMap.containsKey(localeStr)) {
+ connection = sComponentMap.get(localeStr);
+ }
+ if (connection != null && !resetIfChanged) {
+ return connection;
+ }
+ try {
+ final SpellCheckerInfo sci = sService.getCurrentSpellChecker(localeStr);
+ if (sci == null) {
+ return null;
+ }
+ if (connection != null
+ && connection.getSpellCheckerInfo().getId().equals(sci.getId())) {
+ return connection;
+ }
+ connection = new SpellCheckerConnection(localeStr, sci);
+ final Intent serviceIntent = new Intent(SpellCheckerService.SERVICE_INTERFACE);
+
+ serviceIntent.setComponent(sci.getComponent());
+ if (!context.bindService(serviceIntent, connection,
+ Context.BIND_AUTO_CREATE)) {
+ Log.e(TAG, "Bind to spell checker service failed.");
+ return null;
+ }
+ sComponentMap.put(localeStr, connection);
+ return connection;
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Callback for getting results from TextService
+ */
+ public interface Callback {
+ /**
+ * Callback for "isCorrect"
+ * @param text the input for isCorrect
+ * @param start the input for isCorrect
+ * @param end the input for isCorrect
+ * @param locale the input for isCorrect
+ * @param result true if the specified text is a correct word.
+ */
+ public void isCorrectResult(
+ CharSequence text, int start, int end, Locale locale, boolean result);
+ /**
+ * Callback for "getSuggestions"
+ * @param text the input for getSuggestions
+ * @param start the input for getSuggestions
+ * @param end the input for getSuggestions
+ * @param locale the input for getSuggestions
+ * @param result text with "SuggestionSpan"s attached over CharSequence
+ */
+ public void getSuggestionsResult(
+ CharSequence text, int start, int end, Locale locale, CharSequence result);
+ }
+}
diff --git a/core/java/com/android/internal/view/ITextServiceManager.aidl b/core/java/com/android/internal/view/ITextServiceManager.aidl
new file mode 100644
index 0000000..b288221
--- /dev/null
+++ b/core/java/com/android/internal/view/ITextServiceManager.aidl
@@ -0,0 +1,28 @@
+/*
+ * 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 com.android.internal.view;
+
+import android.content.ComponentName;
+import android.view.inputmethod.SpellCheckerInfo;
+
+/**
+ * Public interface to the global input text manager, used by all client
+ * applications.
+ */
+interface ITextServiceManager {
+ SpellCheckerInfo getCurrentSpellChecker(String locale);
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 47902a8..6cdda57 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1112,6 +1112,13 @@
android:description="@string/permdesc_bindInputMethod"
android:protectionLevel="signature" />
+ <!-- Must be required by a TextService (e.g. SpellCheckerService)
+ to ensure that only the system can bind to it. -->
+ <permission android:name="android.permission.BIND_TEXT_SERVICE"
+ android:label="@string/permlab_bindTextService"
+ android:description="@string/permdesc_bindTextService"
+ android:protectionLevel="signature" />
+
<!-- Must be required by a {@link android.service.wallpaper.WallpaperService},
to ensure that only the system can bind to it. -->
<permission android:name="android.permission.BIND_WALLPAPER"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b5f4084..156f9e2 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -705,6 +705,12 @@
interface of an input method. Should never be needed for normal applications.</string>
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_bindTextService">bind to a text service</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_bindTextService">Allows the holder to bind to the top-level
+ interface of a text service(e.g. SpellCheckerService). Should never be needed for normal applications.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permlab_bindWallpaper">bind to a wallpaper</string>
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_bindWallpaper">Allows the holder to bind to the top-level
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index a23bacf..1fc8701 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -228,6 +228,7 @@ class ServerThread extends Thread {
WallpaperManagerService wallpaper = null;
LocationManagerService location = null;
CountryDetectorService countryDetector = null;
+ TextServiceManagerService tsms = null;
if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) {
try {
@@ -271,6 +272,14 @@ class ServerThread extends Thread {
}
try {
+ Slog.i(TAG, "Text Service Manager Service");
+ tsms = new TextServiceManagerService(context);
+ ServiceManager.addService(Context.TEXT_SERVICE_MANAGER_SERVICE, tsms);
+ } catch (Throwable e) {
+ Slog.e(TAG, "Failure starting Text Service Manager Service", e);
+ }
+
+ try {
Slog.i(TAG, "NetworkStats Service");
networkStats = new NetworkStatsService(context, networkManagement, alarm);
ServiceManager.addService(Context.NETWORK_STATS_SERVICE, networkStats);
@@ -534,6 +543,7 @@ class ServerThread extends Thread {
final LocationManagerService locationF = location;
final CountryDetectorService countryDetectorF = countryDetector;
final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater;
+ final TextServiceManagerService textServiceManagerServiceF = tsms;
// We now tell the activity manager it is okay to run third party
// code. It will call back into us once it has gotten to the state
@@ -566,6 +576,7 @@ class ServerThread extends Thread {
if (countryDetectorF != null) countryDetectorF.systemReady();
if (throttleF != null) throttleF.systemReady();
if (networkTimeUpdaterF != null) networkTimeUpdaterF.systemReady();
+ if (textServiceManagerServiceF != null) textServiceManagerServiceF.systemReady();
}
});
diff --git a/services/java/com/android/server/TextServiceManagerService.java b/services/java/com/android/server/TextServiceManagerService.java
new file mode 100644
index 0000000..05e8d53
--- /dev/null
+++ b/services/java/com/android/server/TextServiceManagerService.java
@@ -0,0 +1,169 @@
+/*
+ * 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 com.android.server;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.view.ITextServiceManager;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.SpellCheckerInfo;
+import android.view.inputmethod.SpellCheckerService;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class TextServiceManagerService extends ITextServiceManager.Stub {
+ private static final String TAG = TextServiceManagerService.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ private final Context mContext;
+ private boolean mSystemReady;
+ private final TextServiceMonitor mMonitor;
+ private final HashMap<String, SpellCheckerInfo> mSpellCheckerMap =
+ new HashMap<String, SpellCheckerInfo>();
+ private final ArrayList<SpellCheckerInfo> mSpellCheckerList = new ArrayList<SpellCheckerInfo>();
+
+ public void systemReady() {
+ if (!mSystemReady) {
+ mSystemReady = true;
+ }
+ }
+
+ public TextServiceManagerService(Context context) {
+ mSystemReady = false;
+ mContext = context;
+ mMonitor = new TextServiceMonitor();
+ mMonitor.register(context, true);
+ synchronized (mSpellCheckerMap) {
+ buildSpellCheckerMapLocked(context, mSpellCheckerList, mSpellCheckerMap);
+ }
+ }
+
+ private class TextServiceMonitor extends PackageMonitor {
+ @Override
+ public void onSomePackagesChanged() {
+ synchronized (mSpellCheckerMap) {
+ buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap);
+ // TODO: Update for each locale
+ final SpellCheckerInfo sci = getCurrentSpellChecker(null);
+ final String packageName = sci.getPackageName();
+ final int change = isPackageDisappearing(packageName);
+ if (change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE) {
+ // Package disappearing
+ setCurSpellChecker(findAvailSpellCheckerLocked(null, packageName));
+ } else if (isPackageModified(packageName)) {
+ // Package modified
+ setCurSpellChecker(findAvailSpellCheckerLocked(null, packageName));
+ }
+ }
+ }
+ }
+
+ // Not used for now
+ private SpellCheckerInfo getAppearedPackageLocked(Context context, PackageMonitor monitor) {
+ final int N = mSpellCheckerList.size();
+ for (int i = 0; i < N; ++i) {
+ final SpellCheckerInfo sci = mSpellCheckerList.get(i);
+ String packageName = sci.getPackageName();
+ if (monitor.isPackageAppearing(packageName)
+ == PackageMonitor.PACKAGE_PERMANENT_CHANGE) {
+ return sci;
+ }
+ }
+ return null;
+ }
+
+ private static void buildSpellCheckerMapLocked(Context context,
+ ArrayList<SpellCheckerInfo> list, HashMap<String, SpellCheckerInfo> map) {
+ list.clear();
+ map.clear();
+ final PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> services = pm.queryIntentServices(
+ new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
+ final int N = services.size();
+ for (int i = 0; i < N; ++i) {
+ final ResolveInfo ri = services.get(i);
+ final ServiceInfo si = ri.serviceInfo;
+ final ComponentName compName = new ComponentName(si.packageName, si.name);
+ if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) {
+ Slog.w(TAG, "Skipping text service " + compName
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_TEXT_SERVICE);
+ continue;
+ }
+ if (DBG) Slog.d(TAG, "Add: " + compName);
+ final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
+ list.add(sci);
+ map.put(sci.getId(), sci);
+ }
+ }
+
+ // TODO: find an appropriate spell checker for specified locale
+ private SpellCheckerInfo findAvailSpellCheckerLocked(String locale, String prefPackage) {
+ final int spellCheckersCount = mSpellCheckerList.size();
+ if (spellCheckersCount == 0) {
+ Slog.w(TAG, "no available spell checker services found");
+ return null;
+ }
+ if (prefPackage != null) {
+ for (int i = 0; i < spellCheckersCount; ++i) {
+ final SpellCheckerInfo sci = mSpellCheckerList.get(i);
+ if (prefPackage.equals(sci.getPackageName())) {
+ return sci;
+ }
+ }
+ }
+ if (spellCheckersCount > 1) {
+ Slog.w(TAG, "more than one spell checker service found, picking first");
+ }
+ return mSpellCheckerList.get(0);
+ }
+
+ // TODO: Save SpellCheckerService by supported languages. Currently only one spell
+ // checker is saved.
+ @Override
+ public SpellCheckerInfo getCurrentSpellChecker(String locale) {
+ synchronized (mSpellCheckerMap) {
+ final String curSpellCheckerId =
+ Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.SPELL_CHECKER_SERVICE);
+ if (TextUtils.isEmpty(curSpellCheckerId)) {
+ return null;
+ }
+ return mSpellCheckerMap.get(curSpellCheckerId);
+ }
+ }
+
+ private void setCurSpellChecker(SpellCheckerInfo sci) {
+ Settings.Secure.putString(mContext.getContentResolver(),
+ Settings.Secure.SPELL_CHECKER_SERVICE, sci == null ? "" : sci.getId());
+ }
+}