Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5357a3b3 authored by Nikolas Havrikov's avatar Nikolas Havrikov Committed by Android (Google) Code Review
Browse files

Merge "Don't call IMMS on focusing across non-edit views"

parents 765cc7e7 cb0a7808
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
@@ -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)));
    }
}
+95 −0
Original line number Diff line number Diff line
@@ -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;
@@ -397,6 +398,18 @@ public final class InputMethodManager {
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
    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.
     */
@@ -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);
+10 −0
Original line number Diff line number Diff line
@@ -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);
    }
}
+64 −0
Original line number Diff line number Diff line
/*
 * 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)));
    }
}
+66 −0
Original line number Diff line number Diff line
@@ -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));
    }
}
Loading