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

Commit 3901dc49 authored by Yohei Yukawa's avatar Yohei Yukawa Committed by Android (Google) Code Review
Browse files

Merge "Ensure View#onInputConnectionClosedInternal() timing"

parents c588c268 d55ddf27
Loading
Loading
Loading
Loading
+46 −50
Original line number Diff line number Diff line
@@ -121,7 +121,35 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub {
            // reportFinish() will take effect.
            return;
        }
        closeConnection();
        dispatch(() -> {
            // Note that we do not need to worry about race condition here, because 1) mFinished is
            // updated only inside this block, and 2) the code here is running on a Handler hence we
            // assume multiple closeConnection() tasks will not be handled at the same time.
            if (isFinished()) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#closeConnection");
            try {
                InputConnection ic = getInputConnection();
                // Note we do NOT check isActive() here, because this is safe
                // for an IME to call at any time, and we need to allow it
                // through to clean up our state after the IME has switched to
                // another client.
                if (ic == null) {
                    return;
                }
                @MissingMethodFlags
                final int missingMethods = InputConnectionInspector.getMissingMethodFlags(ic);
                if ((missingMethods & MissingMethodFlags.CLOSE_CONNECTION) == 0) {
                    ic.closeConnection();
                }
            } finally {
                synchronized (mLock) {
                    mInputConnection = null;
                    mFinished = true;
                }
                Trace.traceEnd(Trace.TRACE_TAG_INPUT);
            }

            // Notify the app that the InputConnection was closed.
            final View servedView = mServedView.get();
@@ -140,6 +168,7 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub {
                    }
                }
            }
        });
    }

    @Override
@@ -705,39 +734,6 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub {
        });
    }

    private void closeConnection() {
        dispatch(() -> {
            // Note that we do not need to worry about race condition here, because 1) mFinished is
            // updated only inside this block, and 2) the code here is running on a Handler hence we
            // assume multiple closeConnection() tasks will not be handled at the same time.
            if (isFinished()) {
                return;
            }
            Trace.traceBegin(Trace.TRACE_TAG_INPUT, "InputConnection#closeConnection");
            try {
                InputConnection ic = getInputConnection();
                // Note we do NOT check isActive() here, because this is safe
                // for an IME to call at any time, and we need to allow it
                // through to clean up our state after the IME has switched to
                // another client.
                if (ic == null) {
                    return;
                }
                @MissingMethodFlags
                final int missingMethods = InputConnectionInspector.getMissingMethodFlags(ic);
                if ((missingMethods & MissingMethodFlags.CLOSE_CONNECTION) == 0) {
                    ic.closeConnection();
                }
            } finally {
                synchronized (mLock) {
                    mInputConnection = null;
                    mFinished = true;
                }
                Trace.traceEnd(Trace.TRACE_TAG_INPUT);
            }
        });
    }

    @Override
    public void commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts,
            IBooleanResultCallback callback) {
+301 −0
Original line number Diff line number Diff line
@@ -19,13 +19,25 @@ package android.view;
import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.annotation.DurationMillisLong;
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.text.InputType;
import android.text.format.DateUtils;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
@@ -45,12 +57,23 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

/**
 * Tests for internal APIs/behaviors of {@link View} and {@link InputConnection}.
 */
@MediumTest
@RunWith(AndroidJUnit4.class)
public class ViewInputConnectionTest {
    @DurationMillisLong
    private static final long TIMEOUT = 5000;
    @DurationMillisLong
    private static final long EXPECTED_TIMEOUT = 500;

    @Rule
    public ActivityTestRule<ViewInputConnectionTestActivity> mActivityRule =
            new ActivityTestRule<>(ViewInputConnectionTestActivity.class);
@@ -289,4 +312,282 @@ public class ViewInputConnectionTest {
            super.onInputConnectionClosedInternal();
        }
    }


    @Test
    public void testInputConnectionCallbacks_nonUiThread() throws Throwable {
        try (InputConnectionHandlingThread thread = new InputConnectionHandlingThread()) {
            final ViewGroup viewGroup = getOnMainSync(() -> mActivity.findViewById(R.id.root));
            final TestOffThreadEditor editor = getOnMainSync(() -> {
                final TestOffThreadEditor myEditor =
                        new TestOffThreadEditor(viewGroup.getContext(), thread.getHandler());
                viewGroup.addView(myEditor);
                myEditor.requestFocus();
                return myEditor;
            });

            mInstrumentation.waitForIdleSync();

            assertThat(editor.mOnCreateInputConnectionCalled.await(TIMEOUT, TimeUnit.MILLISECONDS))
                    .isTrue();

            // Invalidate the currently used InputConnection by moving the focus to a new EditText.
            mActivityRule.runOnUiThread(() -> {
                final EditText editText = new EditText(viewGroup.getContext());
                viewGroup.addView(editText);
                editText.requestFocus();
            });

            // Make sure that InputConnection#closeConnection() gets called on the handler thread.
            assertThat(editor.mInputConnectionClosedCalled.await(TIMEOUT, TimeUnit.MILLISECONDS))
                    .isTrue();
            assertThat(editor.mInputConnectionClosedCallingThreadId.get())
                    .isEqualTo(thread.getThreadId());

            // Make sure that View#onInputConnectionClosed() is not yet dispatched, because
            // InputConnection#closeConnection() is still blocked.
            assertThat(editor.mOnInputConnectionClosedCalled.await(
                    EXPECTED_TIMEOUT, TimeUnit.MILLISECONDS)).isFalse();

            // Unblock InputConnection#closeConnection()
            editor.mInputConnectionClosedBlocker.countDown();

            // Make sure that View#onInputConnectionClosed() is dispatched on the main thread.
            assertThat(editor.mOnInputConnectionClosedCalled.await(TIMEOUT, TimeUnit.MILLISECONDS))
                    .isTrue();
            assertThat(editor.mInputConnectionClosedBlockerTimedOut.get()).isFalse();
            assertThat(editor.mOnInputConnectionClosedCallingThreadId.get())
                    .isEqualTo(getOnMainSync(Process::myTid));
        }
    }

    private <T> T getOnMainSync(@NonNull Supplier<T> supplier) throws Throwable {
        final AtomicReference<T> result = new AtomicReference<>();
        mActivityRule.runOnUiThread(() -> result.set(supplier.get()));
        return result.get();
    }

    private static class TestOffThreadEditor extends View {
        private static final int TEST_VIEW_HEIGHT = 10;

        public CountDownLatch mOnCreateInputConnectionCalled = new CountDownLatch(1);
        public CountDownLatch mInputConnectionClosedCalled = new CountDownLatch(1);
        public CountDownLatch mInputConnectionClosedBlocker = new CountDownLatch(1);
        public AtomicBoolean mInputConnectionClosedBlockerTimedOut = new AtomicBoolean();
        public AtomicReference<Integer> mInputConnectionClosedCallingThreadId =
                new AtomicReference<>();

        public CountDownLatch mOnInputConnectionClosedCalled = new CountDownLatch(1);
        public AtomicReference<Integer> mOnInputConnectionClosedCallingThreadId =
                new AtomicReference<>();

        private final Handler mInputConnectionHandler;

        TestOffThreadEditor(Context context, @NonNull Handler inputConnectionHandler) {
            super(context);
            setBackgroundColor(Color.YELLOW);
            setLayoutParams(new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, TEST_VIEW_HEIGHT));
            setFocusableInTouchMode(true);
            setFocusable(true);
            mInputConnectionHandler = inputConnectionHandler;
        }

        @Override
        public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
            mOnCreateInputConnectionCalled.countDown();
            outAttrs.inputType = InputType.TYPE_CLASS_TEXT;
            return new NoOpInputConnection() {
                @Override
                public Handler getHandler() {
                    return mInputConnectionHandler;
                }

                @Override
                public void closeConnection() {
                    mInputConnectionClosedCallingThreadId.compareAndSet(null, Process.myTid());
                    mInputConnectionClosedCalled.countDown();
                    try {
                        if (mInputConnectionClosedBlocker.await(TIMEOUT, TimeUnit.MILLISECONDS)) {
                            return;
                        }
                    } catch (InterruptedException e) {
                    }
                    mInputConnectionClosedBlockerTimedOut.set(true);
                }
            };
        }

        @Override
        public boolean onCheckIsTextEditor() {
            return true;
        }

        @Override
        public void onInputConnectionClosedInternal() {
            mOnInputConnectionClosedCallingThreadId.compareAndSet(null, Process.myTid());
            mOnInputConnectionClosedCalled.countDown();
            super.onInputConnectionClosedInternal();
        }
    }

    static class NoOpInputConnection implements InputConnection {

        @Override
        public CharSequence getTextBeforeCursor(int n, int flags) {
            return null;
        }

        @Override
        public CharSequence getTextAfterCursor(int n, int flags) {
            return null;
        }

        @Override
        public CharSequence getSelectedText(int flags) {
            return null;
        }

        @Override
        public int getCursorCapsMode(int reqModes) {
            return 0;
        }

        @Override
        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
            return null;
        }

        @Override
        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
            return false;
        }

        @Override
        public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
            return false;
        }

        @Override
        public boolean setComposingText(CharSequence text, int newCursorPosition) {
            return false;
        }

        @Override
        public boolean setComposingRegion(int start, int end) {
            return false;
        }

        @Override
        public boolean finishComposingText() {
            return false;
        }

        @Override
        public boolean commitText(CharSequence text, int newCursorPosition) {
            return false;
        }

        @Override
        public boolean commitCompletion(CompletionInfo text) {
            return false;
        }

        @Override
        public boolean commitCorrection(CorrectionInfo correctionInfo) {
            return false;
        }

        @Override
        public boolean setSelection(int start, int end) {
            return false;
        }

        @Override
        public boolean performEditorAction(int editorAction) {
            return false;
        }

        @Override
        public boolean performContextMenuAction(int id) {
            return false;
        }

        @Override
        public boolean beginBatchEdit() {
            return false;
        }

        @Override
        public boolean endBatchEdit() {
            return false;
        }

        @Override
        public boolean sendKeyEvent(KeyEvent event) {
            return false;
        }

        @Override
        public boolean clearMetaKeyStates(int states) {
            return false;
        }

        @Override
        public boolean reportFullscreenMode(boolean enabled) {
            return false;
        }

        @Override
        public boolean performPrivateCommand(String action, Bundle data) {
            return false;
        }

        @Override
        public boolean requestCursorUpdates(int cursorUpdateMode) {
            return false;
        }

        @Override
        public Handler getHandler() {
            return null;
        }

        @Override
        public void closeConnection() {

        }

        @Override
        public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
            return false;
        }
    }

    private static final class InputConnectionHandlingThread extends HandlerThread
            implements AutoCloseable {

        private final Handler mHandler;

        InputConnectionHandlingThread() {
            super("IC-callback");
            start();
            mHandler = Handler.createAsync(getLooper());
        }

        @NonNull
        Handler getHandler() {
            return mHandler;
        }

        @Override
        public void close() {
            quitSafely();
            try {
                join(TIMEOUT);
            } catch (InterruptedException e) {
                fail("Failed to stop the thread: " + e);
            }
        }
    }
}