diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c430a4d573383..4f05a599c1718 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -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.
+ *
+ *
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.
+ *
+ *
The default implementation does nothing, since a view doesn't support input methods by
+ * default (see {@link #onCreateInputConnection}).
+ *
+ *
Note: 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
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index b8f04159faa90..5785999e26726 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -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
@@ -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.
@@ -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) {
@@ -1990,6 +2009,7 @@ public final class InputMethodManager {
} else {
servedContext = null;
missingMethodFlags = 0;
+ icHandler = null;
}
mServedInputConnectionWrapper = servedContext;
@@ -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);
@@ -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;
}
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 56f18d5dc1d75..e28a28587854f 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -161,6 +161,14 @@
+
+
+
+
+
+
diff --git a/core/tests/coretests/res/layout/activity_view_ic_test.xml b/core/tests/coretests/res/layout/activity_view_ic_test.xml
new file mode 100644
index 0000000000000..fa26869c59974
--- /dev/null
+++ b/core/tests/coretests/res/layout/activity_view_ic_test.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/core/tests/coretests/src/android/view/ViewInputConnectionTest.java b/core/tests/coretests/src/android/view/ViewInputConnectionTest.java
new file mode 100644
index 0000000000000..d667af39f5857
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewInputConnectionTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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 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();
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/view/ViewInputConnectionTestActivity.java b/core/tests/coretests/src/android/view/ViewInputConnectionTestActivity.java
new file mode 100644
index 0000000000000..55c812df6a6ae
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewInputConnectionTestActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+public class ViewInputConnectionTestActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_view_ic_test);
+ }
+}