Loading core/java/android/view/View.java +36 −0 Original line number Diff line number Diff line Loading @@ -15164,6 +15164,42 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } /** * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application * that the system has successfully initialized an {@link InputConnection} and it is ready for * use. * * <p>The default implementation does nothing, since a view doesn't support input methods by * default (see {@link #onCreateInputConnection}). * * @param inputConnection The {@link InputConnection} from {@link #onCreateInputConnection}, * after it's been fully initialized by the system. * @param editorInfo The {@link EditorInfo} that was used to create the {@link InputConnection}. * @param handler The dedicated {@link Handler} on which IPC method calls from input methods * will be dispatched. This is the handler returned by {@link InputConnection#getHandler()}. If * that method returns null, this parameter will be null also. * * @hide */ public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) {} /** * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application * that the {@link InputConnection} has been closed. * * <p>The default implementation does nothing, since a view doesn't support input methods by * default (see {@link #onCreateInputConnection}). * * <p><strong>Note:</strong> This callback is not invoked if the view is already detached when * the {@link InputConnection} is closed or the connection is not valid and managed by * {@link com.android.server.inputmethod.InputMethodManagerService}. * TODO(b/170645312): Before un-hiding this API, handle the detached view scenario. * * @hide */ public void onInputConnectionClosedInternal() {} /** * Called by the {@link android.view.inputmethod.InputMethodManager} * when a view who is not the current core/java/android/view/inputmethod/InputMethodManager.java +31 −2 Original line number Diff line number Diff line Loading @@ -1006,6 +1006,24 @@ public final class InputMethodManager { return; } closeConnection(); // Notify the app that the InputConnection was closed. final View servedView = mServedView.get(); if (servedView != null) { final Handler handler = servedView.getHandler(); // The handler is null if the view is already detached. When that's the case, for // now, we simply don't dispatch this callback. if (handler != null) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionClosed: view=" + servedView); } if (handler.getLooper().isCurrentThread()) { servedView.onInputConnectionClosedInternal(); } else { handler.post(servedView::onInputConnectionClosedInternal); } } } } @Override Loading Loading @@ -1940,6 +1958,8 @@ public final class InputMethodManager { InputConnection ic = view.onCreateInputConnection(tba); if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); final Handler icHandler; InputBindResult res = null; synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. Loading Loading @@ -1976,7 +1996,6 @@ public final class InputMethodManager { mCursorCandEnd = -1; mCursorRect.setEmpty(); mCursorAnchorInfo = null; final Handler icHandler; missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic); if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER) != 0) { Loading @@ -1990,6 +2009,7 @@ public final class InputMethodManager { } else { servedContext = null; missingMethodFlags = 0; icHandler = null; } mServedInputConnectionWrapper = servedContext; Loading @@ -1997,7 +2017,7 @@ public final class InputMethodManager { if (DEBUG) Log.v(TAG, "START INPUT: view=" + dumpViewInfo(view) + " ic=" + ic + " tba=" + tba + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags)); final InputBindResult res = mService.startInputOrWindowGainedFocus( res = mService.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, tba, servedContext, missingMethodFlags, view.getContext().getApplicationInfo().targetSdkVersion); Loading Loading @@ -2036,6 +2056,15 @@ public final class InputMethodManager { } } // Notify the app that the InputConnection is initialized and ready for use. if (ic != null && res != null && res.method != null) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view + ", ic=" + ic + ", tba=" + tba + ", handler=" + icHandler); } view.onInputConnectionOpenedInternal(ic, tba, icHandler); } return true; } Loading core/tests/coretests/AndroidManifest.xml +8 −0 Original line number Diff line number Diff line Loading @@ -164,6 +164,14 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="android.view.ViewInputConnectionTestActivity" android:label="View Input Connection Test" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="StubTestBrowserActivity" android:label="Stubbed Test Browser" android:exported="true"> Loading core/tests/coretests/res/layout/activity_view_ic_test.xml 0 → 100644 +23 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2020 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. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout> core/tests/coretests/src/android/view/ViewInputConnectionTest.java 0 → 100644 +292 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; import android.app.Instrumentation; import android.content.Context; import android.os.Handler; import android.text.format.DateUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; import com.android.compatibility.common.util.PollingCheck; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Tests for internal APIs/behaviors of {@link View} and {@link InputConnection}. */ @MediumTest @RunWith(AndroidJUnit4.class) public class ViewInputConnectionTest { @Rule public ActivityTestRule<ViewInputConnectionTestActivity> mActivityRule = new ActivityTestRule<>(ViewInputConnectionTestActivity.class); private Instrumentation mInstrumentation; private ViewInputConnectionTestActivity mActivity; private InputMethodManager mImm; @Before public void before() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mActivity = mActivityRule.getActivity(); PollingCheck.waitFor(5 * DateUtils.SECOND_IN_MILLIS, mActivity::hasWindowFocus); assertTrue(mActivity.hasWindowFocus()); mImm = mActivity.getSystemService(InputMethodManager.class); } @Test public void testInputConnectionCallbacks() throws Throwable { // Add two EditText inputs to the layout view. final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestEditText editText1 = new TestEditText(mActivity, false); final TestEditText editText2 = new TestEditText(mActivity, false); mActivityRule.runOnUiThread(() -> { viewGroup.addView(editText1); viewGroup.addView(editText2); }); mInstrumentation.waitForIdleSync(); // Focus into the first EditText. mActivityRule.runOnUiThread(editText1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isTrue(); assertThat(editText2.isFocused()).isFalse(); // Show the IME for the first EditText. Assert that the appropriate opened/closed callbacks // have been invoked (InputConnection opened for the first EditText). mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText1, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isFalse(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); // Focus into the second EditText. mActivityRule.runOnUiThread(editText2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isFalse(); assertThat(editText2.isFocused()).isTrue(); // Show the IME for the second EditText. Assert that the appropriate opened/closed callbacks // have been invoked (InputConnection closed for the first EditText and opened for the // second EditText). mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText2, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText1.mCalledOnInputConnectionClosed).isTrue(); assertThat(editText2.mCalledOnCreateInputConnection).isTrue(); assertThat(editText2.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); } @Test public void testInputConnectionCallbacks_nullInputConnection() throws Throwable { // Add two EditText inputs to the layout view. final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestEditText editText1 = new TestEditText(mActivity, true); final TestEditText editText2 = new TestEditText(mActivity, true); mActivityRule.runOnUiThread(() -> { viewGroup.addView(editText1); viewGroup.addView(editText2); }); mInstrumentation.waitForIdleSync(); // Focus into the first EditText. mActivityRule.runOnUiThread(editText1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isTrue(); assertThat(editText2.isFocused()).isFalse(); // Show the IME for the first EditText. Assert that the opened/closed callbacks are not // invoked since there's no input connection. mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText1, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isFalse(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); // Focus into the second EditText. mActivityRule.runOnUiThread(editText2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isFalse(); assertThat(editText2.isFocused()).isTrue(); // Show the IME for the second EditText. Assert that the opened/closed callbacks are not // invoked since there's no input connection. mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText2, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isTrue(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); } @Test public void testInputConnectionCallbacks_nonEditableInput() throws Throwable { final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestButton view1 = new TestButton(mActivity); final TestButton view2 = new TestButton(mActivity); mActivityRule.runOnUiThread(() -> { viewGroup.addView(view1); viewGroup.addView(view2); }); mInstrumentation.waitForIdleSync(); // Request focus + IME on the first view. mActivityRule.runOnUiThread(view1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(view1.isFocused()).isTrue(); assertThat(view2.isFocused()).isFalse(); mActivityRule.runOnUiThread(() -> mImm.showSoftInput(view1, 0)); mInstrumentation.waitForIdleSync(); // Assert that the opened/closed callbacks are not invoked since there's no InputConnection. mActivityRule.runOnUiThread(() -> { assertThat(view1.mCalledOnCreateInputConnection).isTrue(); assertThat(view1.mCalledOnInputConnectionOpened).isFalse(); assertThat(view1.mCalledOnInputConnectionClosed).isFalse(); assertThat(view2.mCalledOnCreateInputConnection).isFalse(); assertThat(view2.mCalledOnInputConnectionOpened).isFalse(); assertThat(view2.mCalledOnInputConnectionClosed).isFalse(); }); // Request focus + IME on the second view. mActivityRule.runOnUiThread(view2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(view1.isFocused()).isFalse(); assertThat(view2.isFocused()).isTrue(); mActivityRule.runOnUiThread(() -> mImm.showSoftInput(view1, 0)); mInstrumentation.waitForIdleSync(); // Assert that the opened/closed callbacks are not invoked since there's no InputConnection. mActivityRule.runOnUiThread(() -> { assertThat(view1.mCalledOnCreateInputConnection).isTrue(); assertThat(view1.mCalledOnInputConnectionOpened).isFalse(); assertThat(view1.mCalledOnInputConnectionClosed).isFalse(); assertThat(view2.mCalledOnCreateInputConnection).isTrue(); assertThat(view2.mCalledOnInputConnectionOpened).isFalse(); assertThat(view2.mCalledOnInputConnectionClosed).isFalse(); }); } private static class TestEditText extends EditText { private final boolean mReturnNullInputConnection; public boolean mCalledOnCreateInputConnection = false; public boolean mCalledOnInputConnectionOpened = false; public boolean mCalledOnInputConnectionClosed = false; TestEditText(Context context, boolean returnNullInputConnection) { super(context); mReturnNullInputConnection = returnNullInputConnection; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { mCalledOnCreateInputConnection = true; if (mReturnNullInputConnection) { return null; } else { return super.onCreateInputConnection(outAttrs); } } @Override public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) { mCalledOnInputConnectionOpened = true; super.onInputConnectionOpenedInternal(inputConnection, editorInfo, handler); } @Override public void onInputConnectionClosedInternal() { mCalledOnInputConnectionClosed = true; super.onInputConnectionClosedInternal(); } } private static class TestButton extends Button { public boolean mCalledOnCreateInputConnection = false; public boolean mCalledOnInputConnectionOpened = false; public boolean mCalledOnInputConnectionClosed = false; TestButton(Context context) { super(context); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { mCalledOnCreateInputConnection = true; return super.onCreateInputConnection(outAttrs); } @Override public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) { mCalledOnInputConnectionOpened = true; super.onInputConnectionOpenedInternal(inputConnection, editorInfo, handler); } @Override public void onInputConnectionClosedInternal() { mCalledOnInputConnectionClosed = true; super.onInputConnectionClosedInternal(); } } } Loading
core/java/android/view/View.java +36 −0 Original line number Diff line number Diff line Loading @@ -15164,6 +15164,42 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } /** * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application * that the system has successfully initialized an {@link InputConnection} and it is ready for * use. * * <p>The default implementation does nothing, since a view doesn't support input methods by * default (see {@link #onCreateInputConnection}). * * @param inputConnection The {@link InputConnection} from {@link #onCreateInputConnection}, * after it's been fully initialized by the system. * @param editorInfo The {@link EditorInfo} that was used to create the {@link InputConnection}. * @param handler The dedicated {@link Handler} on which IPC method calls from input methods * will be dispatched. This is the handler returned by {@link InputConnection#getHandler()}. If * that method returns null, this parameter will be null also. * * @hide */ public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) {} /** * Called by the {@link android.view.inputmethod.InputMethodManager} to notify the application * that the {@link InputConnection} has been closed. * * <p>The default implementation does nothing, since a view doesn't support input methods by * default (see {@link #onCreateInputConnection}). * * <p><strong>Note:</strong> This callback is not invoked if the view is already detached when * the {@link InputConnection} is closed or the connection is not valid and managed by * {@link com.android.server.inputmethod.InputMethodManagerService}. * TODO(b/170645312): Before un-hiding this API, handle the detached view scenario. * * @hide */ public void onInputConnectionClosedInternal() {} /** * Called by the {@link android.view.inputmethod.InputMethodManager} * when a view who is not the current
core/java/android/view/inputmethod/InputMethodManager.java +31 −2 Original line number Diff line number Diff line Loading @@ -1006,6 +1006,24 @@ public final class InputMethodManager { return; } closeConnection(); // Notify the app that the InputConnection was closed. final View servedView = mServedView.get(); if (servedView != null) { final Handler handler = servedView.getHandler(); // The handler is null if the view is already detached. When that's the case, for // now, we simply don't dispatch this callback. if (handler != null) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionClosed: view=" + servedView); } if (handler.getLooper().isCurrentThread()) { servedView.onInputConnectionClosedInternal(); } else { handler.post(servedView::onInputConnectionClosedInternal); } } } } @Override Loading Loading @@ -1940,6 +1958,8 @@ public final class InputMethodManager { InputConnection ic = view.onCreateInputConnection(tba); if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic); final Handler icHandler; InputBindResult res = null; synchronized (mH) { // Now that we are locked again, validate that our state hasn't // changed. Loading Loading @@ -1976,7 +1996,6 @@ public final class InputMethodManager { mCursorCandEnd = -1; mCursorRect.setEmpty(); mCursorAnchorInfo = null; final Handler icHandler; missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic); if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER) != 0) { Loading @@ -1990,6 +2009,7 @@ public final class InputMethodManager { } else { servedContext = null; missingMethodFlags = 0; icHandler = null; } mServedInputConnectionWrapper = servedContext; Loading @@ -1997,7 +2017,7 @@ public final class InputMethodManager { if (DEBUG) Log.v(TAG, "START INPUT: view=" + dumpViewInfo(view) + " ic=" + ic + " tba=" + tba + " startInputFlags=" + InputMethodDebug.startInputFlagsToString(startInputFlags)); final InputBindResult res = mService.startInputOrWindowGainedFocus( res = mService.startInputOrWindowGainedFocus( startInputReason, mClient, windowGainingFocus, startInputFlags, softInputMode, windowFlags, tba, servedContext, missingMethodFlags, view.getContext().getApplicationInfo().targetSdkVersion); Loading Loading @@ -2036,6 +2056,15 @@ public final class InputMethodManager { } } // Notify the app that the InputConnection is initialized and ready for use. if (ic != null && res != null && res.method != null) { if (DEBUG) { Log.v(TAG, "Calling View.onInputConnectionOpened: view= " + view + ", ic=" + ic + ", tba=" + tba + ", handler=" + icHandler); } view.onInputConnectionOpenedInternal(ic, tba, icHandler); } return true; } Loading
core/tests/coretests/AndroidManifest.xml +8 −0 Original line number Diff line number Diff line Loading @@ -164,6 +164,14 @@ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="android.view.ViewInputConnectionTestActivity" android:label="View Input Connection Test" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" /> </intent-filter> </activity> <activity android:name="StubTestBrowserActivity" android:label="Stubbed Test Browser" android:exported="true"> Loading
core/tests/coretests/res/layout/activity_view_ic_test.xml 0 → 100644 +23 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2020 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. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout>
core/tests/coretests/src/android/view/ViewInputConnectionTest.java 0 → 100644 +292 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; import android.app.Instrumentation; import android.content.Context; import android.os.Handler; import android.text.format.DateUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; import com.android.compatibility.common.util.PollingCheck; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Tests for internal APIs/behaviors of {@link View} and {@link InputConnection}. */ @MediumTest @RunWith(AndroidJUnit4.class) public class ViewInputConnectionTest { @Rule public ActivityTestRule<ViewInputConnectionTestActivity> mActivityRule = new ActivityTestRule<>(ViewInputConnectionTestActivity.class); private Instrumentation mInstrumentation; private ViewInputConnectionTestActivity mActivity; private InputMethodManager mImm; @Before public void before() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mActivity = mActivityRule.getActivity(); PollingCheck.waitFor(5 * DateUtils.SECOND_IN_MILLIS, mActivity::hasWindowFocus); assertTrue(mActivity.hasWindowFocus()); mImm = mActivity.getSystemService(InputMethodManager.class); } @Test public void testInputConnectionCallbacks() throws Throwable { // Add two EditText inputs to the layout view. final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestEditText editText1 = new TestEditText(mActivity, false); final TestEditText editText2 = new TestEditText(mActivity, false); mActivityRule.runOnUiThread(() -> { viewGroup.addView(editText1); viewGroup.addView(editText2); }); mInstrumentation.waitForIdleSync(); // Focus into the first EditText. mActivityRule.runOnUiThread(editText1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isTrue(); assertThat(editText2.isFocused()).isFalse(); // Show the IME for the first EditText. Assert that the appropriate opened/closed callbacks // have been invoked (InputConnection opened for the first EditText). mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText1, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isFalse(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); // Focus into the second EditText. mActivityRule.runOnUiThread(editText2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isFalse(); assertThat(editText2.isFocused()).isTrue(); // Show the IME for the second EditText. Assert that the appropriate opened/closed callbacks // have been invoked (InputConnection closed for the first EditText and opened for the // second EditText). mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText2, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText1.mCalledOnInputConnectionClosed).isTrue(); assertThat(editText2.mCalledOnCreateInputConnection).isTrue(); assertThat(editText2.mCalledOnInputConnectionOpened).isTrue(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); } @Test public void testInputConnectionCallbacks_nullInputConnection() throws Throwable { // Add two EditText inputs to the layout view. final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestEditText editText1 = new TestEditText(mActivity, true); final TestEditText editText2 = new TestEditText(mActivity, true); mActivityRule.runOnUiThread(() -> { viewGroup.addView(editText1); viewGroup.addView(editText2); }); mInstrumentation.waitForIdleSync(); // Focus into the first EditText. mActivityRule.runOnUiThread(editText1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isTrue(); assertThat(editText2.isFocused()).isFalse(); // Show the IME for the first EditText. Assert that the opened/closed callbacks are not // invoked since there's no input connection. mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText1, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isFalse(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); // Focus into the second EditText. mActivityRule.runOnUiThread(editText2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(editText1.isFocused()).isFalse(); assertThat(editText2.isFocused()).isTrue(); // Show the IME for the second EditText. Assert that the opened/closed callbacks are not // invoked since there's no input connection. mActivityRule.runOnUiThread(() -> mImm.showSoftInput(editText2, 0)); mInstrumentation.waitForIdleSync(); mActivityRule.runOnUiThread(() -> { assertThat(editText1.mCalledOnCreateInputConnection).isTrue(); assertThat(editText1.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText1.mCalledOnInputConnectionClosed).isFalse(); assertThat(editText2.mCalledOnCreateInputConnection).isTrue(); assertThat(editText2.mCalledOnInputConnectionOpened).isFalse(); assertThat(editText2.mCalledOnInputConnectionClosed).isFalse(); }); } @Test public void testInputConnectionCallbacks_nonEditableInput() throws Throwable { final ViewGroup viewGroup = mActivity.findViewById(R.id.root); final TestButton view1 = new TestButton(mActivity); final TestButton view2 = new TestButton(mActivity); mActivityRule.runOnUiThread(() -> { viewGroup.addView(view1); viewGroup.addView(view2); }); mInstrumentation.waitForIdleSync(); // Request focus + IME on the first view. mActivityRule.runOnUiThread(view1::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(view1.isFocused()).isTrue(); assertThat(view2.isFocused()).isFalse(); mActivityRule.runOnUiThread(() -> mImm.showSoftInput(view1, 0)); mInstrumentation.waitForIdleSync(); // Assert that the opened/closed callbacks are not invoked since there's no InputConnection. mActivityRule.runOnUiThread(() -> { assertThat(view1.mCalledOnCreateInputConnection).isTrue(); assertThat(view1.mCalledOnInputConnectionOpened).isFalse(); assertThat(view1.mCalledOnInputConnectionClosed).isFalse(); assertThat(view2.mCalledOnCreateInputConnection).isFalse(); assertThat(view2.mCalledOnInputConnectionOpened).isFalse(); assertThat(view2.mCalledOnInputConnectionClosed).isFalse(); }); // Request focus + IME on the second view. mActivityRule.runOnUiThread(view2::requestFocus); mInstrumentation.waitForIdleSync(); assertThat(view1.isFocused()).isFalse(); assertThat(view2.isFocused()).isTrue(); mActivityRule.runOnUiThread(() -> mImm.showSoftInput(view1, 0)); mInstrumentation.waitForIdleSync(); // Assert that the opened/closed callbacks are not invoked since there's no InputConnection. mActivityRule.runOnUiThread(() -> { assertThat(view1.mCalledOnCreateInputConnection).isTrue(); assertThat(view1.mCalledOnInputConnectionOpened).isFalse(); assertThat(view1.mCalledOnInputConnectionClosed).isFalse(); assertThat(view2.mCalledOnCreateInputConnection).isTrue(); assertThat(view2.mCalledOnInputConnectionOpened).isFalse(); assertThat(view2.mCalledOnInputConnectionClosed).isFalse(); }); } private static class TestEditText extends EditText { private final boolean mReturnNullInputConnection; public boolean mCalledOnCreateInputConnection = false; public boolean mCalledOnInputConnectionOpened = false; public boolean mCalledOnInputConnectionClosed = false; TestEditText(Context context, boolean returnNullInputConnection) { super(context); mReturnNullInputConnection = returnNullInputConnection; } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { mCalledOnCreateInputConnection = true; if (mReturnNullInputConnection) { return null; } else { return super.onCreateInputConnection(outAttrs); } } @Override public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) { mCalledOnInputConnectionOpened = true; super.onInputConnectionOpenedInternal(inputConnection, editorInfo, handler); } @Override public void onInputConnectionClosedInternal() { mCalledOnInputConnectionClosed = true; super.onInputConnectionClosedInternal(); } } private static class TestButton extends Button { public boolean mCalledOnCreateInputConnection = false; public boolean mCalledOnInputConnectionOpened = false; public boolean mCalledOnInputConnectionClosed = false; TestButton(Context context) { super(context); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { mCalledOnCreateInputConnection = true; return super.onCreateInputConnection(outAttrs); } @Override public void onInputConnectionOpenedInternal(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable Handler handler) { mCalledOnInputConnectionOpened = true; super.onInputConnectionOpenedInternal(inputConnection, editorInfo, handler); } @Override public void onInputConnectionClosedInternal() { mCalledOnInputConnectionClosed = true; super.onInputConnectionClosedInternal(); } } }