diff --git a/core/api/current.txt b/core/api/current.txt index e1cb5e4f062a3..8bc603d8513c9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -52315,7 +52315,7 @@ package android.view.textservice { method @NonNull public java.util.List getEnabledSpellCheckerInfos(); method public boolean isSpellCheckerEnabled(); method @Nullable public android.view.textservice.SpellCheckerSession newSpellCheckerSession(@Nullable android.os.Bundle, @Nullable java.util.Locale, @NonNull android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener, boolean); - method @Nullable public android.view.textservice.SpellCheckerSession newSpellCheckerSession(@Nullable android.os.Bundle, @Nullable java.util.Locale, @NonNull android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener, boolean, int); + method @Nullable public android.view.textservice.SpellCheckerSession newSpellCheckerSession(@Nullable java.util.Locale, boolean, int, @Nullable android.os.Bundle, @NonNull java.util.concurrent.Executor, @NonNull android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener); } } diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java index ba58b6525a6de..a449cf1876e12 100644 --- a/core/java/android/view/textservice/SpellCheckerSession.java +++ b/core/java/android/view/textservice/SpellCheckerSession.java @@ -16,6 +16,8 @@ package android.view.textservice; +import android.annotation.BinderThread; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.os.Binder; import android.os.Build; @@ -27,6 +29,7 @@ import android.os.RemoteException; import android.util.Log; import android.view.inputmethod.InputMethodManager; +import com.android.internal.annotations.GuardedBy; import com.android.internal.textservice.ISpellCheckerSession; import com.android.internal.textservice.ISpellCheckerSessionListener; import com.android.internal.textservice.ITextServicesSessionListener; @@ -35,6 +38,7 @@ import dalvik.system.CloseGuard; import java.util.LinkedList; import java.util.Queue; +import java.util.concurrent.Executor; /** * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. @@ -102,38 +106,26 @@ public class SpellCheckerSession { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private final SpellCheckerSessionListener mSpellCheckerSessionListener; private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; + private final Executor mExecutor; 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, TextServicesManager tsm, SpellCheckerSessionListener listener) { + SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener, + Executor executor) { if (info == null || listener == null || tsm == null) { throw new NullPointerException(); } mSpellCheckerInfo = info; - mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); + mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(this); mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); mTextServicesManager = tsm; mSpellCheckerSessionListener = listener; + mExecutor = executor; mGuard.open("finishSession"); } @@ -219,12 +211,13 @@ public class SpellCheckerSession { textInfos, suggestionsLimit, sequentialWords); } - private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { - mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); + void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos) { + mExecutor.execute(() -> mSpellCheckerSessionListener.onGetSuggestions(suggestionsInfos)); } - private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) { - mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos); + void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos) { + mExecutor.execute(() -> + mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionsInfos)); } private static final class SpellCheckerSessionListenerImpl @@ -249,7 +242,8 @@ public class SpellCheckerSession { } private final Queue mPendingTasks = new LinkedList<>(); - private Handler mHandler; + @GuardedBy("SpellCheckerSessionListenerImpl.this") + private SpellCheckerSession mSpellCheckerSession; private static final int STATE_WAIT_CONNECTION = 0; private static final int STATE_CONNECTED = 1; @@ -270,8 +264,8 @@ public class SpellCheckerSession { private HandlerThread mThread; private Handler mAsyncHandler; - public SpellCheckerSessionListenerImpl(Handler handler) { - mHandler = handler; + SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession) { + mSpellCheckerSession = spellCheckerSession; } private static class SpellCheckerParams { @@ -349,6 +343,7 @@ public class SpellCheckerSession { } } + @GuardedBy("SpellCheckerSessionListenerImpl.this") private void processCloseLocked() { if (DBG) Log.d(TAG, "entering processCloseLocked:" + " session" + (mISpellCheckerSession != null ? ".hashCode()=#" @@ -358,7 +353,7 @@ public class SpellCheckerSession { if (mThread != null) { mThread.quit(); } - mHandler = null; + mSpellCheckerSession = null; mPendingTasks.clear(); mThread = null; mAsyncHandler = null; @@ -502,23 +497,30 @@ public class SpellCheckerSession { processTask(session, scp, false); } + @BinderThread @Override public void onGetSuggestions(SuggestionsInfo[] results) { - synchronized (this) { - if (mHandler != null) { - mHandler.sendMessage(Message.obtain(mHandler, - MSG_ON_GET_SUGGESTION_MULTIPLE, results)); - } + SpellCheckerSession session = getSpellCheckerSession(); + if (session != null) { + // Lock should not be held when calling callback, in order to avoid deadlock. + session.handleOnGetSuggestionsMultiple(results); } } + @BinderThread @Override public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { - synchronized (this) { - if (mHandler != null) { - mHandler.sendMessage(Message.obtain(mHandler, - MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); - } + SpellCheckerSession session = getSpellCheckerSession(); + if (session != null) { + // Lock should not be held when calling callback, in order to avoid deadlock. + session.handleOnGetSentenceSuggestionsMultiple(results); + } + } + + @Nullable + private SpellCheckerSession getSpellCheckerSession() { + synchronized (SpellCheckerSessionListenerImpl.this) { + return mSpellCheckerSession; } } } diff --git a/core/java/android/view/textservice/TextServicesManager.java b/core/java/android/view/textservice/TextServicesManager.java index 4f6fa272e1030..bf91cca855228 100644 --- a/core/java/android/view/textservice/TextServicesManager.java +++ b/core/java/android/view/textservice/TextServicesManager.java @@ -16,6 +16,7 @@ package android.view.textservice; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -25,6 +26,8 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerExecutor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -40,6 +43,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.Executor; /** * System API to the overall text services, which arbitrates interaction between applications @@ -161,10 +166,12 @@ public final class TextServicesManager { * {@link SuggestionsInfo#RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS} will be passed to the spell * checker as supported attributes. * - * @see #newSpellCheckerSession(Bundle, Locale, SpellCheckerSessionListener, boolean, int) + * @see #newSpellCheckerSession(Locale, boolean, int, Bundle, Executor, + * SpellCheckerSessionListener) * @param bundle A bundle to pass to the spell checker. * @param locale The locale for the spell checker. * @param listener A spell checker session lister for getting results from the spell checker. + * The listener will be called on the calling thread. * @param referToSpellCheckerLanguageSettings If true, the session for one of enabled * languages in settings will be used. * @return A spell checker session from the spell checker. @@ -174,10 +181,15 @@ public final class TextServicesManager { @Nullable Locale locale, @NonNull SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) { - return newSpellCheckerSession(bundle, locale, listener, referToSpellCheckerLanguageSettings, - SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY - | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO - | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS); + // Attributes existed before {@link #newSpellCheckerSession(Locale, boolean, int, Bundle, + // Executor, SpellCheckerSessionListener)} was introduced. + int supportedAttributes = SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY + | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO + | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS; + // Using the implicit looper to preserve the old behavior. + Executor executor = new HandlerExecutor(new Handler()); + return newSpellCheckerSession(locale, referToSpellCheckerLanguageSettings, + supportedAttributes, bundle, executor, listener); } /** @@ -191,25 +203,28 @@ public final class TextServicesManager { * language only (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be * selected. * - * @param bundle A bundle to pass to the spell checker. * @param locale The locale for the spell checker. - * @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 used. * @param supportedAttributes A union of {@link SuggestionsInfo} attributes that the spell * checker can set in the spell checking results. + * @param bundle A bundle for passing implementation-specific extra parameters for the spell + * checker. You can check the current spell checker package by + * {@link #getCurrentSpellCheckerInfo()}. + * @param executor An executor to call the listener on. + * @param listener A spell checker session lister for getting results from a spell checker. * @return The spell checker session of the spell checker. */ @Nullable public SpellCheckerSession newSpellCheckerSession( - @SuppressLint("NullableCollection") @Nullable Bundle bundle, @SuppressLint("UseIcu") @Nullable Locale locale, - @NonNull SpellCheckerSessionListener listener, - @SuppressLint("ListenerLast") boolean referToSpellCheckerLanguageSettings, - @SuppressLint("ListenerLast") @SuggestionsInfo.ResultAttrs int supportedAttributes) { - if (listener == null) { - throw new NullPointerException(); - } + boolean referToSpellCheckerLanguageSettings, + @SuggestionsInfo.ResultAttrs int supportedAttributes, + @Nullable Bundle bundle, + @NonNull @CallbackExecutor Executor executor, + @NonNull SpellCheckerSessionListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); if (!referToSpellCheckerLanguageSettings && locale == null) { throw new IllegalArgumentException("Locale should not be null if you don't refer" + " settings."); @@ -259,7 +274,7 @@ public final class TextServicesManager { if (subtypeInUse == null) { return null; } - final SpellCheckerSession session = new SpellCheckerSession(sci, this, listener); + final SpellCheckerSession session = new SpellCheckerSession(sci, this, listener, executor); try { mService.getSpellCheckerService(mUserId, sci.getId(), subtypeInUse.getLocale(), session.getTextServicesSessionListener(), diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index 2464b4af23eba..a63305e55e538 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -126,14 +126,14 @@ public class SpellChecker implements SpellCheckerSessionListener { || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { mSpellCheckerSession = null; } else { + int supportedAttributes = SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY + | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO + | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR + | SuggestionsInfo.RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS; mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( + mCurrentLocale, false, supportedAttributes, null /* Bundle not currently used by the textServicesManager */, - mCurrentLocale, this, - false /* means any available languages from current spell checker */, - SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY - | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO - | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_GRAMMAR_ERROR - | SuggestionsInfo.RESULT_ATTR_DONT_SHOW_UI_FOR_SUGGESTIONS); + mTextView.getContext().getMainExecutor(), this); } // Restore SpellCheckSpans in pool