InputMethodInfo attr to suppress spell checker.

If InputMethodService declares suppressesSpellChecker="true", the system
SpellCheckerService will be disabled.

With TextView, when IMSs are switched while the user is editing,
SuggestionSpans that were previously added will not be removed even if
the next IMS declares suppressesSpellChecker="true".
This is because we don't know if the SuggestionSpans were from Spell
Checker, App, or IMS.

Bug: 153473490
Test: atest CtsInputMethodTestCases:SpellCheckerTest
Change-Id: Ia22b8758111087818beea8c07b1c173f1a94b8e4
This commit is contained in:
Kohsuke Yatoh
2021-02-18 16:03:38 -08:00
parent 11a7f13364
commit 6b22120e8f
11 changed files with 100 additions and 14 deletions

View File

@@ -1394,6 +1394,7 @@ package android {
field public static final int supportsRtl = 16843695; // 0x10103af
field public static final int supportsSwitchingToNextInputMethod = 16843755; // 0x10103eb
field public static final int supportsUploading = 16843419; // 0x101029b
field public static final int suppressesSpellChecker = 16844354; // 0x1010642
field public static final int switchMinWidth = 16843632; // 0x1010370
field public static final int switchPadding = 16843633; // 0x1010371
field public static final int switchPreferenceStyle = 16843629; // 0x101036d
@@ -51469,6 +51470,7 @@ package android.view.inputmethod {
method public int getSubtypeCount();
method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
method public CharSequence loadLabel(android.content.pm.PackageManager);
method public boolean suppressesSpellChecker();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InputMethodInfo> CREATOR;
}
@@ -51490,6 +51492,7 @@ package android.view.inputmethod {
method public boolean isActive(android.view.View);
method public boolean isActive();
method public boolean isFullscreenMode();
method public boolean isInputMethodSuppressingSpellChecker();
method @Deprecated public boolean isWatchingCursor(android.view.View);
method public void restartInput(android.view.View);
method public void sendAppPrivateCommand(android.view.View, String, android.os.Bundle);

View File

@@ -60,6 +60,7 @@ import java.util.List;
* @attr ref android.R.styleable#InputMethod_isDefault
* @attr ref android.R.styleable#InputMethod_supportsSwitchingToNextInputMethod
* @attr ref android.R.styleable#InputMethod_supportsInlineSuggestions
* @attr ref android.R.styleable#InputMethod_suppressesSpellChecker
*/
public final class InputMethodInfo implements Parcelable {
static final String TAG = "InputMethodInfo";
@@ -117,6 +118,11 @@ public final class InputMethodInfo implements Parcelable {
*/
private final boolean mInlineSuggestionsEnabled;
/**
* The flag whether this IME suppresses spell checker.
*/
private final boolean mSuppressesSpellChecker;
/**
* @param service the {@link ResolveInfo} corresponds in which the IME is implemented.
* @return a unique ID to be returned by {@link #getId()}. We have used
@@ -160,6 +166,7 @@ public final class InputMethodInfo implements Parcelable {
boolean isAuxIme = true;
boolean supportsSwitchingToNextInputMethod = false; // false as default
boolean inlineSuggestionsEnabled = false; // false as default
boolean suppressesSpellChecker = false; // false as default
mForceDefault = false;
PackageManager pm = context.getPackageManager();
@@ -203,6 +210,8 @@ public final class InputMethodInfo implements Parcelable {
false);
inlineSuggestionsEnabled = sa.getBoolean(
com.android.internal.R.styleable.InputMethod_supportsInlineSuggestions, false);
suppressesSpellChecker = sa.getBoolean(
com.android.internal.R.styleable.InputMethod_suppressesSpellChecker, false);
sa.recycle();
final int depth = parser.getDepth();
@@ -274,6 +283,7 @@ public final class InputMethodInfo implements Parcelable {
mIsAuxIme = isAuxIme;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
mSuppressesSpellChecker = suppressesSpellChecker;
mIsVrOnly = isVrOnly;
}
@@ -284,6 +294,7 @@ public final class InputMethodInfo implements Parcelable {
mIsAuxIme = source.readInt() == 1;
mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
mInlineSuggestionsEnabled = source.readInt() == 1;
mSuppressesSpellChecker = source.readBoolean();
mIsVrOnly = source.readBoolean();
mService = ResolveInfo.CREATOR.createFromParcel(source);
mSubtypes = new InputMethodSubtypeArray(source);
@@ -342,6 +353,7 @@ public final class InputMethodInfo implements Parcelable {
mForceDefault = forceDefault;
mSupportsSwitchingToNextInputMethod = supportsSwitchingToNextInputMethod;
mInlineSuggestionsEnabled = inlineSuggestionsEnabled;
mSuppressesSpellChecker = false;
mIsVrOnly = isVrOnly;
}
@@ -494,7 +506,8 @@ public final class InputMethodInfo implements Parcelable {
+ " mSettingsActivityName=" + mSettingsActivityName
+ " mIsVrOnly=" + mIsVrOnly
+ " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod
+ " mInlineSuggestionsEnabled=" + mInlineSuggestionsEnabled);
+ " mInlineSuggestionsEnabled=" + mInlineSuggestionsEnabled
+ " mSuppressesSpellChecker=" + mSuppressesSpellChecker);
pw.println(prefix + "mIsDefaultResId=0x"
+ Integer.toHexString(mIsDefaultResId));
pw.println(prefix + "Service:");
@@ -562,6 +575,13 @@ public final class InputMethodInfo implements Parcelable {
return mInlineSuggestionsEnabled;
}
/**
* Return {@code true} if this input method suppresses spell checker.
*/
public boolean suppressesSpellChecker() {
return mSuppressesSpellChecker;
}
/**
* Used to package this object into a {@link Parcel}.
*
@@ -576,6 +596,7 @@ public final class InputMethodInfo implements Parcelable {
dest.writeInt(mIsAuxIme ? 1 : 0);
dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
dest.writeInt(mInlineSuggestionsEnabled ? 1 : 0);
dest.writeBoolean(mSuppressesSpellChecker);
dest.writeBoolean(mIsVrOnly);
mService.writeToParcel(dest, flags);
mSubtypes.writeToParcel(dest);

View File

@@ -444,6 +444,13 @@ public final class InputMethodManager {
*/
private Matrix mActivityViewToScreenMatrix = null;
/**
* As reported by {@link InputBindResult}. This value is determined by
* {@link com.android.internal.R.styleable#InputMethod_suppressesSpellChecking}.
*/
@GuardedBy("mH")
private boolean mIsInputMethodSuppressingSpellChecker = false;
// -----------------------------------------------------------
/**
@@ -858,6 +865,8 @@ public final class InputMethodManager {
mCurId = res.id;
mBindSequence = res.sequence;
mActivityViewToScreenMatrix = res.getActivityViewToScreenMatrix();
mIsInputMethodSuppressingSpellChecker =
res.isInputMethodSuppressingSpellChecker;
}
startInputInner(StartInputReason.BOUND_TO_IMMS, null, 0, 0, 0);
return;
@@ -1469,6 +1478,15 @@ public final class InputMethodManager {
}
}
/**
* Return {@code true} if the input method is suppressing system spell checker.
*/
public boolean isInputMethodSuppressingSpellChecker() {
synchronized (mH) {
return mIsInputMethodSuppressingSpellChecker;
}
}
/**
* Reset all of the state associated with being bound to an input method.
*/
@@ -1513,6 +1531,7 @@ public final class InputMethodManager {
@UnsupportedAppUsage
void finishInputLocked() {
mActivityViewToScreenMatrix = null;
mIsInputMethodSuppressingSpellChecker = false;
setNextServedViewLocked(null);
if (getServedViewLocked() != null) {
if (DEBUG) {
@@ -2037,6 +2056,7 @@ public final class InputMethodManager {
return false;
}
mActivityViewToScreenMatrix = res.getActivityViewToScreenMatrix();
mIsInputMethodSuppressingSpellChecker = res.isInputMethodSuppressingSpellChecker;
if (res.id != null) {
setInputChannelLocked(res.channel);
mBindSequence = res.sequence;

View File

@@ -25,6 +25,7 @@ import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.textservice.ISpellCheckerSession;
import com.android.internal.textservice.ISpellCheckerSessionListener;
@@ -176,6 +177,11 @@ public class SpellCheckerSession {
* @param suggestionsLimit the maximum number of suggestions that will be returned
*/
public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
handleOnGetSentenceSuggestionsMultiple(new SentenceSuggestionsInfo[0]);
return;
}
mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
textInfos, suggestionsLimit);
}
@@ -204,6 +210,11 @@ public class SpellCheckerSession {
if (DBG) {
Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
}
final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
handleOnGetSuggestionsMultiple(new SuggestionsInfo[0]);
return;
}
mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
textInfos, suggestionsLimit, sequentialWords);
}

View File

@@ -30,6 +30,7 @@ import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.os.UserHandle;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
import com.android.internal.textservice.ISpellCheckerSessionListener;
@@ -88,10 +89,15 @@ public final class TextServicesManager {
@UserIdInt
private final int mUserId;
private TextServicesManager(@UserIdInt int userId) throws ServiceNotFoundException {
@Nullable
private final InputMethodManager mInputMethodManager;
private TextServicesManager(@UserIdInt int userId,
@Nullable InputMethodManager inputMethodManager) throws ServiceNotFoundException {
mService = ITextServicesManager.Stub.asInterface(
ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
mUserId = userId;
mInputMethodManager = inputMethodManager;
}
/**
@@ -105,7 +111,8 @@ public final class TextServicesManager {
@NonNull
public static TextServicesManager createInstance(@NonNull Context context)
throws ServiceNotFoundException {
return new TextServicesManager(context.getUserId());
return new TextServicesManager(context.getUserId(), context.getSystemService(
InputMethodManager.class));
}
/**
@@ -118,7 +125,7 @@ public final class TextServicesManager {
synchronized (TextServicesManager.class) {
if (sInstance == null) {
try {
sInstance = new TextServicesManager(UserHandle.myUserId());
sInstance = new TextServicesManager(UserHandle.myUserId(), null);
} catch (ServiceNotFoundException e) {
throw new IllegalStateException(e);
}
@@ -127,6 +134,12 @@ public final class TextServicesManager {
}
}
/** @hide */
@Nullable
public InputMethodManager getInputMethodManager() {
return mInputMethodManager;
}
/**
* Returns the language component of a given locale string.
*/

View File

@@ -988,6 +988,12 @@ public class Editor {
if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
&& !(mTextView.isInExtractedMode())) {
final InputMethodManager imm = getInputMethodManager();
if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
// Do not close mSpellChecker here as it may be reused when the current IME has been
// changed.
return;
}
if (mSpellChecker == null && createSpellChecker) {
mSpellChecker = new SpellChecker(mTextView);
}

View File

@@ -204,6 +204,8 @@ public final class InputBindResult implements Parcelable {
@Nullable
private final float[] mActivityViewToScreenMatrixValues;
public final boolean isInputMethodSuppressingSpellChecker;
/**
* @return {@link Matrix} that corresponds to {@link #mActivityViewToScreenMatrixValues}.
* {@code null} if {@link #mActivityViewToScreenMatrixValues} is {@code null}.
@@ -220,7 +222,8 @@ public final class InputBindResult implements Parcelable {
public InputBindResult(@ResultCode int _result,
IInputMethodSession _method, InputChannel _channel, String _id, int _sequence,
@Nullable Matrix activityViewToScreenMatrix) {
@Nullable Matrix activityViewToScreenMatrix,
boolean isInputMethodSuppressingSpellChecker) {
result = _result;
method = _method;
channel = _channel;
@@ -232,6 +235,7 @@ public final class InputBindResult implements Parcelable {
mActivityViewToScreenMatrixValues = new float[9];
activityViewToScreenMatrix.getValues(mActivityViewToScreenMatrixValues);
}
this.isInputMethodSuppressingSpellChecker = isInputMethodSuppressingSpellChecker;
}
InputBindResult(Parcel source) {
@@ -245,6 +249,7 @@ public final class InputBindResult implements Parcelable {
id = source.readString();
sequence = source.readInt();
mActivityViewToScreenMatrixValues = source.createFloatArray();
isInputMethodSuppressingSpellChecker = source.readBoolean();
}
@Override
@@ -252,6 +257,7 @@ public final class InputBindResult implements Parcelable {
return "InputBindResult{result=" + getResultString() + " method="+ method + " id=" + id
+ " sequence=" + sequence
+ " activityViewToScreenMatrix=" + getActivityViewToScreenMatrix()
+ " isInputMethodSuppressingSpellChecker=" + isInputMethodSuppressingSpellChecker
+ "}";
}
@@ -274,6 +280,7 @@ public final class InputBindResult implements Parcelable {
dest.writeString(id);
dest.writeInt(sequence);
dest.writeFloatArray(mActivityViewToScreenMatrixValues);
dest.writeBoolean(isInputMethodSuppressingSpellChecker);
}
/**
@@ -340,7 +347,7 @@ public final class InputBindResult implements Parcelable {
}
private static InputBindResult error(@ResultCode int result) {
return new InputBindResult(result, null, null, null, -1, null);
return new InputBindResult(result, null, null, null, -1, null, false);
}
/**

View File

@@ -3564,6 +3564,7 @@
<attr name="__removed2" format="boolean" />
<!-- Specifies whether the IME supports showing inline suggestions. -->
<attr name="supportsInlineSuggestions" format="boolean" />
<attr name="suppressesSpellChecker" format="boolean" />
</declare-styleable>
<!-- This is the subtype of InputMethod. Subtype can describe locales (for example, en_US and

View File

@@ -3088,6 +3088,7 @@
<public name="selectableAsDefault"/>
<public name="isAccessibilityTool"/>
<public name="attributionTags"/>
<public name="suppressesSpellChecker" />
</public-group>
<public-group type="drawable" first-id="0x010800b5">

View File

@@ -2381,9 +2381,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
showCurrentInputLocked(mCurFocusedWindow, getAppShowFlags(), null,
SoftInputShowHideReason.ATTACH_NEW_INPUT);
}
final InputMethodInfo curInputMethodInfo = mMethodMap.get(mCurId);
final boolean suppressesSpellChecker =
curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
session.session, (session.channel != null ? session.channel.dup() : null),
mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
mCurId, mCurSeq, mCurActivityViewToScreenMatrix, suppressesSpellChecker);
}
@Nullable
@@ -2425,7 +2428,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// party code.
return new InputBindResult(
InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
null, null, mCurMethodId, mCurSeq, null);
null, null, mCurMethodId, mCurSeq, null, false);
}
if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.uid,
@@ -2501,7 +2504,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
requestClientSessionLocked(cs);
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
null, null, mCurId, mCurSeq, null);
null, null, mCurId, mCurSeq, null, false);
} else if (SystemClock.uptimeMillis()
< (mLastBindTime+TIME_TO_RECONNECT)) {
// In this case we have connected to the service, but
@@ -2513,7 +2516,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
// to see if we can get back in touch with the service.
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, mCurId, mCurSeq, null);
null, null, mCurId, mCurSeq, null, false);
} else {
EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
mCurMethodId, SystemClock.uptimeMillis()-mLastBindTime, 0);
@@ -2553,7 +2556,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, mCurId, mCurSeq, null);
null, null, mCurId, mCurSeq, null, false);
}
mCurIntent = null;
Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
@@ -3509,7 +3512,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
null, null, null, -1, null);
null, null, null, -1, null, false);
}
mCurFocusedWindow = windowToken;

View File

@@ -1748,7 +1748,7 @@ public final class MultiClientInputMethodManagerService {
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
null, null, data.mCurrentInputMethodInfo.getId(),
clientInfo.mBindingSequence, null);
clientInfo.mBindingSequence, null, false);
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
clientInfo.mBindingSequence++;
@@ -1770,7 +1770,7 @@ public final class MultiClientInputMethodManagerService {
clientInfo.mInputMethodSession,
clientInfo.mWriteChannel.dup(),
data.mCurrentInputMethodInfo.getId(),
clientInfo.mBindingSequence, null);
clientInfo.mBindingSequence, null, false);
case InputMethodClientState.UNREGISTERED:
Slog.e(TAG, "The client is already unregistered.");
return InputBindResult.INVALID_CLIENT;