diff options
-rw-r--r-- | Android.mk | 3 | ||||
-rw-r--r-- | api/current.txt | 48 | ||||
-rw-r--r-- | core/java/android/app/ContextImpl.java | 6 | ||||
-rw-r--r-- | core/java/android/content/Context.java | 9 | ||||
-rw-r--r-- | core/java/android/provider/Settings.java | 12 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/ISpellCheckerService.aidl | 27 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/SpellCheckerInfo.aidl | 19 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/SpellCheckerInfo.java | 109 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/SpellCheckerService.java | 144 | ||||
-rw-r--r-- | core/java/android/view/inputmethod/TextServiceManager.java | 393 | ||||
-rw-r--r-- | core/java/com/android/internal/view/ITextServiceManager.aidl | 28 | ||||
-rw-r--r-- | core/res/AndroidManifest.xml | 7 | ||||
-rwxr-xr-x | core/res/res/values/strings.xml | 6 | ||||
-rw-r--r-- | services/java/com/android/server/SystemServer.java | 11 | ||||
-rw-r--r-- | services/java/com/android/server/TextServiceManagerService.java | 169 |
15 files changed, 988 insertions, 3 deletions
@@ -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()); + } +} |