diff options
6 files changed, 281 insertions, 1 deletions
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index 949237548926..fdff7a30a6e9 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -1099,4 +1099,41 @@ public class EditorInfo implements InputType, Parcelable { public int describeContents() { return 0; } + + /** + * Performs a loose equality check, which means there can be false negatives, but if the method + * returns {@code true}, then both objects are guaranteed to be equal. + * <ul> + * <li>{@link #extras} is compared with {@link Bundle#kindofEquals}</li> + * <li>{@link #actionLabel}, {@link #hintText}, and {@link #label} are compared with + * {@link TextUtils#equals}, which does not account for Spans. </li> + * </ul> + * @hide + */ + public boolean kindofEquals(@Nullable EditorInfo that) { + if (that == null) return false; + if (this == that) return true; + return inputType == that.inputType + && imeOptions == that.imeOptions + && internalImeOptions == that.internalImeOptions + && actionId == that.actionId + && initialSelStart == that.initialSelStart + && initialSelEnd == that.initialSelEnd + && initialCapsMode == that.initialCapsMode + && fieldId == that.fieldId + && Objects.equals(autofillId, that.autofillId) + && Objects.equals(privateImeOptions, that.privateImeOptions) + && Objects.equals(packageName, that.packageName) + && Objects.equals(fieldName, that.fieldName) + && Objects.equals(hintLocales, that.hintLocales) + && Objects.equals(targetInputMethodUser, that.targetInputMethodUser) + && Arrays.equals(contentMimeTypes, that.contentMimeTypes) + && TextUtils.equals(actionLabel, that.actionLabel) + && TextUtils.equals(hintText, that.hintText) + && TextUtils.equals(label, that.label) + && (extras == that.extras || (extras != null && extras.kindofEquals(that.extras))) + && (mInitialSurroundingText == that.mInitialSurroundingText + || (mInitialSurroundingText != null + && mInitialSurroundingText.isEqualTo(that.mInitialSurroundingText))); + } } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index adeed256ebc1..aacd4ec63d9f 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -70,6 +70,7 @@ import android.os.Process; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; +import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; @@ -398,6 +399,18 @@ public final class InputMethodManager { public static final long CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING = 214016041L; // This is a bug id. /** + * If {@code true}, avoid calling the + * {@link com.android.server.inputmethod.InputMethodManagerService InputMethodManagerService} + * by skipping the call to {@link IInputMethodManager#startInputOrWindowGainedFocus} + * when we are switching focus between two non-editable views. This saves the cost of a binder + * call into the system server. + * <p><b>Note:</b> + * The default value is {@code true}. + */ + private static final boolean OPTIMIZE_NONEDITABLE_VIEWS = + SystemProperties.getBoolean("debug.imm.optimize_noneditable_views", true); + + /** * @deprecated Use {@link #mServiceInvoker} instead. */ @Deprecated @@ -646,6 +659,26 @@ public final class InputMethodManager { private final class DelegateImpl implements ImeFocusController.InputMethodManagerDelegate { + @GuardedBy("mH") + @Nullable + private ViewFocusParameterInfo mPreviousViewFocusParameters; + + @GuardedBy("mH") + private void updatePreviousViewFocusParametersLocked( + @Nullable EditorInfo currentEditorInfo, + @StartInputFlags int startInputFlags, + @StartInputReason int startInputReason, + @SoftInputModeFlags int softInputMode, + int windowFlags) { + mPreviousViewFocusParameters = new ViewFocusParameterInfo(currentEditorInfo, + startInputFlags, startInputReason, softInputMode, windowFlags); + } + + @GuardedBy("mH") + private void clearStateLocked() { + mPreviousViewFocusParameters = null; + } + /** * Used by {@link ImeFocusController} to start input connection. */ @@ -1692,8 +1725,10 @@ public final class InputMethodManager { * Reset all of the state associated with a served view being connected * to an input method */ + @GuardedBy("mH") private void clearConnectionLocked() { mCurrentEditorInfo = null; + mDelegate.clearStateLocked(); if (mServedInputConnection != null) { mServedInputConnection.deactivate(); mServedInputConnection = null; @@ -2344,6 +2379,9 @@ public final class InputMethodManager { // Hook 'em up and let 'er rip. mCurrentEditorInfo = tba.createCopyInternal(); + // Store the previously served connection so that we can determine whether it is safe + // to skip the call to startInputOrWindowGainedFocus in the IMMS + final RemoteInputConnectionImpl previouslyServedConnection = mServedInputConnection; mServedConnecting = false; if (mServedInputConnection != null) { @@ -2383,6 +2421,22 @@ public final class InputMethodManager { + ic + " tba=" + tba + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags)); } + + // When we switch between non-editable views, do not call into the IMMS. + final boolean canSkip = OPTIMIZE_NONEDITABLE_VIEWS + && previouslyServedConnection == null + && ic == null + && isSwitchingBetweenEquivalentNonEditableViews( + mDelegate.mPreviousViewFocusParameters, startInputFlags, + startInputReason, softInputMode, windowFlags); + updatePreviousViewFocusParametersLocked(mCurrentEditorInfo, startInputFlags, + startInputReason, softInputMode, windowFlags); + if (canSkip) { + if (DEBUG) { + Log.d(TAG, "Not calling IMMS due to switching between non-editable views."); + } + return false; + } res = mServiceInvoker.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, tba, servedInputConnection, @@ -2445,6 +2499,47 @@ public final class InputMethodManager { return true; } + /** + * This method exists only so that the + * <a href="https://errorprone.info/bugpattern/GuardedBy">errorprone</a> false positive warning + * can be suppressed without granting a blanket exception to the {@link #startInputInner} + * method. + * <p> + * The warning in question implies that the access to the + * {@link DelegateImpl#updatePreviousViewFocusParametersLocked} method should be guarded by + * {@code InputMethodManager.this.mH}, but instead {@code mDelegate.mH} is held in the caller. + * In this case errorprone fails to realize that it is the same object. + */ + @GuardedBy("mH") + @SuppressWarnings("GuardedBy") + private void updatePreviousViewFocusParametersLocked( + @Nullable EditorInfo currentEditorInfo, + @StartInputFlags int startInputFlags, + @StartInputReason int startInputReason, + @SoftInputModeFlags int softInputMode, + int windowFlags) { + mDelegate.updatePreviousViewFocusParametersLocked(currentEditorInfo, startInputFlags, + startInputReason, softInputMode, windowFlags); + } + + /** + * @return {@code true} when we are switching focus between two non-editable views + * so that we can avoid calling {@link IInputMethodManager#startInputOrWindowGainedFocus}. + */ + @GuardedBy("mH") + private boolean isSwitchingBetweenEquivalentNonEditableViews( + @Nullable ViewFocusParameterInfo previousViewFocusParameters, + @StartInputFlags int startInputFlags, + @StartInputReason int startInputReason, + @SoftInputModeFlags int softInputMode, + int windowFlags) { + return (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) == 0 + && (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0 + && previousViewFocusParameters != null + && previousViewFocusParameters.sameAs(mCurrentEditorInfo, + startInputFlags, startInputReason, softInputMode, windowFlags); + } + private void reportInputConnectionOpened( InputConnection ic, EditorInfo tba, Handler icHandler, View view) { view.onInputConnectionOpenedInternal(ic, tba, icHandler); diff --git a/core/java/android/view/inputmethod/SurroundingText.java b/core/java/android/view/inputmethod/SurroundingText.java index c85a18a1df79..6bfd63bafd45 100644 --- a/core/java/android/view/inputmethod/SurroundingText.java +++ b/core/java/android/view/inputmethod/SurroundingText.java @@ -181,4 +181,14 @@ public final class SurroundingText implements Parcelable { } } } + + /** @hide */ + public boolean isEqualTo(@Nullable SurroundingText that) { + if (that == null) return false; + if (this == that) return true; + return mSelectionStart == that.mSelectionStart + && mSelectionEnd == that.mSelectionEnd + && mOffset == that.mOffset + && TextUtils.equals(mText, that.mText); + } } diff --git a/core/java/android/view/inputmethod/ViewFocusParameterInfo.java b/core/java/android/view/inputmethod/ViewFocusParameterInfo.java new file mode 100644 index 000000000000..44c33fac8ccb --- /dev/null +++ b/core/java/android/view/inputmethod/ViewFocusParameterInfo.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 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.annotation.Nullable; +import android.view.WindowManager.LayoutParams.SoftInputModeFlags; + +import com.android.internal.inputmethod.StartInputFlags; +import com.android.internal.inputmethod.StartInputReason; +import com.android.internal.view.IInputMethodManager; + +/** + * This data class is a container for storing the last arguments used when calling into + * {@link IInputMethodManager#startInputOrWindowGainedFocus}. They are used to determine if we + * are switching from a non-editable view to another non-editable view, in which case we avoid + * a binder call into the {@link com.android.server.inputmethod.InputMethodManagerService}. + */ +final class ViewFocusParameterInfo { + @Nullable final EditorInfo mPreviousEditorInfo; + @StartInputFlags final int mPreviousStartInputFlags; + @StartInputReason final int mPreviousStartInputReason; + @SoftInputModeFlags final int mPreviousSoftInputMode; + final int mPreviousWindowFlags; + + ViewFocusParameterInfo(@Nullable EditorInfo previousEditorInfo, + @StartInputFlags int previousStartInputFlags, + @StartInputReason int previousStartInputReason, + @SoftInputModeFlags int previousSoftInputMode, + int previousWindowFlags) { + mPreviousEditorInfo = previousEditorInfo; + mPreviousStartInputFlags = previousStartInputFlags; + mPreviousStartInputReason = previousStartInputReason; + mPreviousSoftInputMode = previousSoftInputMode; + mPreviousWindowFlags = previousWindowFlags; + } + + boolean sameAs(@Nullable EditorInfo currentEditorInfo, + @StartInputFlags int startInputFlags, + @StartInputReason int startInputReason, + @SoftInputModeFlags int softInputMode, + int windowFlags) { + return mPreviousStartInputFlags == startInputFlags + && mPreviousStartInputReason == startInputReason + && mPreviousSoftInputMode == softInputMode + && mPreviousWindowFlags == windowFlags + && (mPreviousEditorInfo == currentEditorInfo + || (mPreviousEditorInfo != null + && mPreviousEditorInfo.kindofEquals(currentEditorInfo))); + } +} diff --git a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java index fe7d28917670..b867e4417439 100644 --- a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java @@ -19,6 +19,7 @@ package android.view.inputmethod; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -59,6 +60,28 @@ public class EditorInfoTest { private static final int TEST_USER_ID = 42; private static final int LONG_EXP_TEXT_LENGTH = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH * 2; + private static final EditorInfo TEST_EDITOR_INFO = new EditorInfo(); + + static { + TEST_EDITOR_INFO.inputType = InputType.TYPE_CLASS_TEXT; // 0x1 + TEST_EDITOR_INFO.imeOptions = EditorInfo.IME_ACTION_GO; // 0x2 + TEST_EDITOR_INFO.privateImeOptions = "testOptions"; + TEST_EDITOR_INFO.initialSelStart = 0; + TEST_EDITOR_INFO.initialSelEnd = 1; + TEST_EDITOR_INFO.initialCapsMode = TextUtils.CAP_MODE_CHARACTERS; // 0x1000 + TEST_EDITOR_INFO.hintText = "testHintText"; + TEST_EDITOR_INFO.label = "testLabel"; + TEST_EDITOR_INFO.packageName = "android.view.inputmethod"; + TEST_EDITOR_INFO.fieldId = 0; + TEST_EDITOR_INFO.autofillId = AutofillId.NO_AUTOFILL_ID; + TEST_EDITOR_INFO.fieldName = "testField"; + TEST_EDITOR_INFO.extras = new Bundle(); + TEST_EDITOR_INFO.extras.putString("testKey", "testValue"); + TEST_EDITOR_INFO.hintLocales = LocaleList.forLanguageTags("en,de,ua"); + TEST_EDITOR_INFO.contentMimeTypes = new String[] {"image/png"}; + TEST_EDITOR_INFO.targetInputMethodUser = UserHandle.of(TEST_USER_ID); + } + /** * Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via * {@link Parcel}. @@ -526,4 +549,47 @@ public class EditorInfoTest { + "prefix: hintLocales=null\n" + "prefix: contentMimeTypes=null\n"); } + + @Test + public void testKindofEqualsAfterCopyInternal() { + final EditorInfo infoCopy = TEST_EDITOR_INFO.createCopyInternal(); + assertTrue(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } + + @Test + public void testKindofEqualsAfterCloneViaParcel() { + // This test demonstrates a false negative case when an EditorInfo is + // created from a Parcel and its extras are still parcelled, which in turn + // runs into the edge case in Bundle.kindofEquals + final EditorInfo infoCopy = cloneViaParcel(TEST_EDITOR_INFO); + assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } + + @Test + public void testKindofEqualsComparesAutofillId() { + final EditorInfo infoCopy = TEST_EDITOR_INFO.createCopyInternal(); + infoCopy.autofillId = new AutofillId(42); + assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } + + @Test + public void testKindofEqualsComparesFieldId() { + final EditorInfo infoCopy = TEST_EDITOR_INFO.createCopyInternal(); + infoCopy.fieldId = 42; + assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } + + @Test + public void testKindofEqualsComparesMimeTypes() { + final EditorInfo infoCopy = TEST_EDITOR_INFO.createCopyInternal(); + infoCopy.contentMimeTypes = new String[] {"image/png", "image/gif"}; + assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } + + @Test + public void testKindofEqualsComparesExtras() { + final EditorInfo infoCopy = TEST_EDITOR_INFO.createCopyInternal(); + infoCopy.extras.putString("testKey2", "testValue"); + assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); + } } diff --git a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java b/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java index dfbc39c76489..50ce335cbec6 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java +++ b/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java @@ -17,7 +17,8 @@ package android.view.inputmethod; import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; import android.os.Parcel; @@ -70,4 +71,11 @@ public class SurroundingTextTest { assertThat(surroundingTextFromParcel.getSelectionEnd(), is(1)); assertThat(surroundingTextFromParcel.getOffset(), is(2)); } + + @Test + public void testIsEqualComparesText() { + final SurroundingText text1 = new SurroundingText("hello", 0, 1, 0); + final SurroundingText text2 = new SurroundingText("there", 0, 1, 0); + assertFalse(text1.isEqualTo(text2)); + } } |