diff --git a/samples/MultiClientInputMethod/Android.mk b/samples/MultiClientInputMethod/Android.mk
new file mode 100755
index 000000000..5d641f985
--- /dev/null
+++ b/samples/MultiClientInputMethod/Android.mk
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2018 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := samples
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := MultiClientInputMethod
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/samples/MultiClientInputMethod/AndroidManifest.xml b/samples/MultiClientInputMethod/AndroidManifest.xml
new file mode 100755
index 000000000..54087769b
--- /dev/null
+++ b/samples/MultiClientInputMethod/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_delete.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_delete.png
new file mode 100755
index 000000000..5139c7179
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_delete.png differ
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_done.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_done.png
new file mode 100755
index 000000000..471c5021b
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_done.png differ
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_return.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_return.png
new file mode 100755
index 000000000..5a5670c32
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_return.png differ
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_search.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_search.png
new file mode 100755
index 000000000..e72cde3bb
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_search.png differ
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_shift.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_shift.png
new file mode 100755
index 000000000..275769618
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_shift.png differ
diff --git a/samples/MultiClientInputMethod/res/drawable/sym_keyboard_space.png b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_space.png
new file mode 100755
index 000000000..cef2daa5d
Binary files /dev/null and b/samples/MultiClientInputMethod/res/drawable/sym_keyboard_space.png differ
diff --git a/samples/MultiClientInputMethod/res/layout/input.xml b/samples/MultiClientInputMethod/res/layout/input.xml
new file mode 100755
index 000000000..528a15361
--- /dev/null
+++ b/samples/MultiClientInputMethod/res/layout/input.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/samples/MultiClientInputMethod/res/xml/method.xml b/samples/MultiClientInputMethod/res/xml/method.xml
new file mode 100644
index 000000000..abac23b5a
--- /dev/null
+++ b/samples/MultiClientInputMethod/res/xml/method.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/samples/MultiClientInputMethod/res/xml/qwerty.xml b/samples/MultiClientInputMethod/res/xml/qwerty.xml
new file mode 100755
index 000000000..6ca76fcc4
--- /dev/null
+++ b/samples/MultiClientInputMethod/res/xml/qwerty.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/ClientCallbackImpl.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/ClientCallbackImpl.java
new file mode 100644
index 000000000..45b4e56f6
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/ClientCallbackImpl.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+final class ClientCallbackImpl implements MultiClientInputMethodServiceDelegate.ClientCallback {
+ private static final String TAG = "ClientCallbackImpl";
+ private static final boolean DEBUG = false;
+
+ private final MultiClientInputMethodServiceDelegate mDelegate;
+ private final SoftInputWindowManager mSoftInputWindowManager;
+ private final int mClientId;
+ private final int mUid;
+ private final int mPid;
+ private final int mSelfReportedDisplayId;
+ private final KeyEvent.DispatcherState mDispatcherState;
+ private final Looper mLooper;
+
+ ClientCallbackImpl(MultiClientInputMethodServiceDelegate delegate,
+ SoftInputWindowManager softInputWindowManager, int clientId, int uid, int pid,
+ int selfReportedDisplayId) {
+ mDelegate = delegate;
+ mSoftInputWindowManager = softInputWindowManager;
+ mClientId = clientId;
+ mUid = uid;
+ mPid = pid;
+ mSelfReportedDisplayId = selfReportedDisplayId;
+ mDispatcherState = new KeyEvent.DispatcherState();
+ // For simplicity, we use the main looper for this sample.
+ // To use other looper thread, make sure that the IME Window also runs on the same looper.
+ mLooper = Looper.getMainLooper();
+ }
+
+ KeyEvent.DispatcherState getDispatcherState() {
+ return mDispatcherState;
+ }
+
+ Looper getLooper() {
+ return mLooper;
+ }
+
+ @Override
+ public void onAppPrivateCommand(String action, Bundle data) {
+ }
+
+ @Override
+ public void onDisplayCompletions(CompletionInfo[] completions) {
+ }
+
+ @Override
+ public void onFinishSession() {
+ if (DEBUG) {
+ Log.v(TAG, "onFinishSession clientId=" + mClientId);
+ }
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
+ if (window == null) {
+ return;
+ }
+ // SoftInputWindow also needs to be cleaned up when this IME client is still associated with
+ // it.
+ if (mClientId == window.getClientId()) {
+ window.onFinishClient();
+ }
+ }
+
+ @Override
+ public void onHideSoftInput(int flags, ResultReceiver resultReceiver) {
+ if (DEBUG) {
+ Log.v(TAG, "onHideSoftInput clientId=" + mClientId + " flags=" + flags);
+ }
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
+ if (window == null) {
+ return;
+ }
+ // Seems that the Launcher3 has a bug to call onHideSoftInput() too early so we cannot
+ // enforce clientId check yet.
+ // TODO: Check clientId like we do so for onShowSoftInput().
+ window.hide();
+ }
+
+ @Override
+ public void onShowSoftInput(int flags, ResultReceiver resultReceiver) {
+ if (DEBUG) {
+ Log.v(TAG, "onShowSoftInput clientId=" + mClientId + " flags=" + flags);
+ }
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
+ if (window == null) {
+ return;
+ }
+ if (mClientId != window.getClientId()) {
+ Log.w(TAG, "onShowSoftInput() from a background client is ignored."
+ + " windowClientId=" + window.getClientId()
+ + " clientId=" + mClientId);
+ return;
+ }
+ window.show();
+ }
+
+ @Override
+ public void onStartInputOrWindowGainedFocus(InputConnection inputConnection,
+ EditorInfo editorInfo, int startInputFlags, int softInputMode, int targetWindowHandle) {
+ if (DEBUG) {
+ Log.v(TAG, "onStartInputOrWindowGainedFocus clientId=" + mClientId
+ + " editorInfo=" + editorInfo
+ + " startInputFlags="
+ + InputMethodDebug.startInputFlagsToString(startInputFlags)
+ + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
+ + " targetWindowHandle=" + targetWindowHandle);
+ }
+
+ final int state = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
+ final boolean forwardNavigation =
+ (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
+
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getOrCreateSoftInputWindow(mSelfReportedDisplayId);
+ if (window == null) {
+ return;
+ }
+
+ if (window.getTargetWindowHandle() != targetWindowHandle) {
+ // Target window has changed. Report new IME target window to the system.
+ mDelegate.reportImeWindowTarget(
+ mClientId, targetWindowHandle, window.getWindow().getAttributes().token);
+ }
+
+ if (inputConnection == null || editorInfo == null) {
+ // Dummy InputConnection case.
+ if (window.getClientId() == mClientId) {
+ // Special hack for temporary focus changes (e.g. notification shade).
+ // If we have already established a connection to this client, and if a dummy
+ // InputConnection is notified, just ignore this event.
+ } else {
+ window.onDummyStartInput(mClientId, targetWindowHandle);
+ }
+ } else {
+ window.onStartInput(mClientId, targetWindowHandle, inputConnection);
+ }
+
+ switch (state) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+ if (forwardNavigation) {
+ window.show();
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+ window.show();
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ if (forwardNavigation) {
+ window.hide();
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ window.hide();
+ break;
+ }
+ }
+
+ @Override
+ public void onToggleSoftInput(int showFlags, int hideFlags) {
+ // TODO: Implement
+ Log.w(TAG, "onToggleSoftInput is not yet implemented. clientId=" + mClientId
+ + " showFlags=" + showFlags + " hideFlags=" + hideFlags);
+ }
+
+ @Override
+ public void onUpdateCursorAnchorInfo(CursorAnchorInfo info) {
+ }
+
+ @Override
+ public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
+ int candidatesStart, int candidatesEnd) {
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (DEBUG) {
+ Log.v(TAG, "onKeyDown clientId=" + mClientId + " keyCode=" + keyCode
+ + " event=" + event);
+ }
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
+ if (window != null && window.isShowing()) {
+ event.startTracking();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (DEBUG) {
+ Log.v(TAG, "onKeyUp clientId=" + mClientId + "keyCode=" + keyCode
+ + " event=" + event);
+ }
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
+ final SoftInputWindow window =
+ mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
+ if (window != null && window.isShowing()) {
+ window.hide();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ return false;
+ }
+}
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/InputMethodDebug.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/InputMethodDebug.java
new file mode 100644
index 000000000..a71bdc892
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/InputMethodDebug.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.view.WindowManager;
+
+import com.android.internal.inputmethod.StartInputFlags;
+
+import java.util.StringJoiner;
+
+/**
+ * Provides useful methods for debugging.
+ */
+final class InputMethodDebug {
+
+ /**
+ * Not intended to be instantiated.
+ */
+ private InputMethodDebug() {
+ }
+
+ /**
+ * Converts soft input flags to {@link String} for debug logging.
+ *
+ * @param softInputMode integer constant for soft input flags.
+ * @return {@link String} message corresponds for the given {@code softInputMode}.
+ */
+ public static String softInputModeToString(int softInputMode) {
+ final StringJoiner joiner = new StringJoiner("|");
+ final int state = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
+ final int adjust = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
+ final boolean isForwardNav =
+ (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
+
+ switch (state) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
+ joiner.add("STATE_UNSPECIFIED");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+ joiner.add("STATE_UNCHANGED");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ joiner.add("STATE_HIDDEN");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ joiner.add("STATE_ALWAYS_HIDDEN");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+ joiner.add("STATE_VISIBLE");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+ joiner.add("STATE_ALWAYS_VISIBLE");
+ break;
+ default:
+ joiner.add("STATE_UNKNOWN(" + state + ")");
+ break;
+ }
+
+ switch (adjust) {
+ case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED:
+ joiner.add("ADJUST_UNSPECIFIED");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE:
+ joiner.add("ADJUST_RESIZE");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN:
+ joiner.add("ADJUST_PAN");
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING:
+ joiner.add("ADJUST_NOTHING");
+ break;
+ default:
+ joiner.add("ADJUST_UNKNOWN(" + adjust + ")");
+ break;
+ }
+
+ if (isForwardNav) {
+ // This is a special bit that is set by the system only during the window navigation.
+ joiner.add("IS_FORWARD_NAVIGATION");
+ }
+
+ return joiner.setEmptyValue("(none)").toString();
+ }
+
+ /**
+ * Converts start input flags to {@link String} for debug logging.
+ *
+ * @param startInputFlags integer constant for start input flags.
+ * @return {@link String} message corresponds for the given {@code startInputFlags}.
+ */
+ public static String startInputFlagsToString(int startInputFlags) {
+ final StringJoiner joiner = new StringJoiner("|");
+ if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) != 0) {
+ joiner.add("VIEW_HAS_FOCUS");
+ }
+ if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) {
+ joiner.add("IS_TEXT_EDITOR");
+ }
+ if ((startInputFlags & StartInputFlags.FIRST_WINDOW_FOCUS_GAIN) != 0) {
+ joiner.add("FIRST_WINDOW_FOCUS_GAIN");
+ }
+ if ((startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0) {
+ joiner.add("INITIAL_CONNECTION");
+ }
+
+ return joiner.setEmptyValue("(none)").toString();
+ }
+}
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/MultiClientInputMethod.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/MultiClientInputMethod.java
new file mode 100644
index 000000000..150c21d04
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/MultiClientInputMethod.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.app.Service;
+import android.content.Intent;
+import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * A {@link Service} that implements multi-client IME protocol.
+ */
+public final class MultiClientInputMethod extends Service {
+ private static final String TAG = "MultiClientInputMethod";
+ private static final boolean DEBUG = false;
+
+ SoftInputWindowManager mSoftInputWindowManager;
+ MultiClientInputMethodServiceDelegate mDelegate;
+
+ @Override
+ public void onCreate() {
+ if (DEBUG) {
+ Log.v(TAG, "onCreate");
+ }
+ mDelegate = MultiClientInputMethodServiceDelegate.create(this,
+ new MultiClientInputMethodServiceDelegate.ServiceCallback() {
+ @Override
+ public void initialized() {
+ if (DEBUG) {
+ Log.i(TAG, "initialized");
+ }
+ }
+
+ @Override
+ public void addClient(int clientId, int uid, int pid,
+ int selfReportedDisplayId) {
+ final ClientCallbackImpl callback = new ClientCallbackImpl(mDelegate,
+ mSoftInputWindowManager, clientId, uid, pid, selfReportedDisplayId);
+ if (DEBUG) {
+ Log.v(TAG, "addClient clientId=" + clientId + " uid=" + uid
+ + " pid=" + pid + " displayId=" + selfReportedDisplayId);
+ }
+ mDelegate.acceptClient(clientId, callback, callback.getDispatcherState(),
+ callback.getLooper());
+ }
+
+ @Override
+ public void removeClient(int clientId) {
+ if (DEBUG) {
+ Log.v(TAG, "removeClient clientId=" + clientId);
+ }
+ }
+ });
+ mSoftInputWindowManager = new SoftInputWindowManager(this, mDelegate);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (DEBUG) {
+ Log.v(TAG, "onBind intent=" + intent);
+ }
+ return mDelegate.onBind(intent);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (DEBUG) {
+ Log.v(TAG, "onUnbind intent=" + intent);
+ }
+ return mDelegate.onUnbind(intent);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) {
+ Log.v(TAG, "onDestroy");
+ }
+ mDelegate.onDestroy();
+ }
+}
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/NoopKeyboardActionListener.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/NoopKeyboardActionListener.java
new file mode 100644
index 000000000..94248ce52
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/NoopKeyboardActionListener.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.inputmethodservice.KeyboardView;
+
+/**
+ * Provides the no-op implementation of {@link KeyboardView.OnKeyboardActionListener}
+ */
+class NoopKeyboardActionListener implements KeyboardView.OnKeyboardActionListener {
+ @Override
+ public void onPress(int primaryCode) {
+ }
+
+ @Override
+ public void onRelease(int primaryCode) {
+ }
+
+ @Override
+ public void onKey(int primaryCode, int[] keyCodes) {
+ }
+
+ @Override
+ public void onText(CharSequence text) {
+ }
+
+ @Override
+ public void swipeLeft() {
+ }
+
+ @Override
+ public void swipeRight() {
+ }
+
+ @Override
+ public void swipeDown() {
+ }
+
+ @Override
+ public void swipeUp() {
+ }
+}
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindow.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindow.java
new file mode 100644
index 000000000..00134fde5
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindow.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.KeyboardView;
+import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.ViewGroup;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.InputConnection;
+import android.widget.LinearLayout;
+
+import java.util.Arrays;
+
+final class SoftInputWindow extends Dialog {
+ private static final String TAG = "SoftInputWindow";
+ private static final boolean DEBUG = false;
+
+ private final KeyboardView mQwerty;
+
+ private int mClientId = MultiClientInputMethodServiceDelegate.INVALID_CLIENT_ID;
+ private int mTargetWindowHandle = MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
+
+ private static final KeyboardView.OnKeyboardActionListener sNoopListener =
+ new NoopKeyboardActionListener();
+
+ SoftInputWindow(Context context, IBinder token) {
+ super(context, android.R.style.Theme_DeviceDefault_InputMethod);
+
+ final LayoutParams lp = getWindow().getAttributes();
+ lp.type = LayoutParams.TYPE_INPUT_METHOD;
+ lp.setTitle("InputMethod");
+ lp.gravity = Gravity.BOTTOM;
+ lp.width = LayoutParams.MATCH_PARENT;
+ lp.height = LayoutParams.WRAP_CONTENT;
+ lp.token = token;
+ getWindow().setAttributes(lp);
+
+ final int windowSetFlags = LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | LayoutParams.FLAG_NOT_FOCUSABLE
+ | LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ final int windowModFlags = LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | LayoutParams.FLAG_NOT_FOCUSABLE
+ | LayoutParams.FLAG_DIM_BEHIND
+ | LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+ getWindow().setFlags(windowSetFlags, windowModFlags);
+
+ final LinearLayout layout = new LinearLayout(context);
+ layout.setOrientation(LinearLayout.VERTICAL);
+
+ mQwerty = (KeyboardView) getLayoutInflater().inflate(R.layout.input, null);
+ mQwerty.setKeyboard(new Keyboard(context, R.xml.qwerty));
+ mQwerty.setOnKeyboardActionListener(sNoopListener);
+ layout.addView(mQwerty);
+
+ setContentView(layout, new ViewGroup.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+
+ // TODO: Check why we need to call this.
+ getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ int getClientId() {
+ return mClientId;
+ }
+
+ int getTargetWindowHandle() {
+ return mTargetWindowHandle;
+ }
+
+ void onFinishClient() {
+ mQwerty.setOnKeyboardActionListener(sNoopListener);
+ mClientId = MultiClientInputMethodServiceDelegate.INVALID_CLIENT_ID;
+ mTargetWindowHandle = MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
+ }
+
+ void onDummyStartInput(int clientId, int targetWindowHandle) {
+ if (DEBUG) {
+ Log.v(TAG, "onDummyStartInput clientId=" + clientId
+ + " targetWindowHandle=" + targetWindowHandle);
+ }
+ mQwerty.setOnKeyboardActionListener(sNoopListener);
+ mClientId = clientId;
+ mTargetWindowHandle = targetWindowHandle;
+ }
+
+ void onStartInput(int clientId, int targetWindowHandle, InputConnection inputConnection) {
+ if (DEBUG) {
+ Log.v(TAG, "onStartInput clientId=" + clientId
+ + " targetWindowHandle=" + targetWindowHandle);
+ }
+ mClientId = clientId;
+ mTargetWindowHandle = targetWindowHandle;
+ mQwerty.setOnKeyboardActionListener(new NoopKeyboardActionListener() {
+ @Override
+ public void onKey(int primaryCode, int[] keyCodes) {
+ if (DEBUG) {
+ Log.v(TAG, "onKey clientId=" + clientId + " primaryCode=" + primaryCode
+ + " keyCodes=" + Arrays.toString(keyCodes));
+ }
+ switch (primaryCode) {
+ case Keyboard.KEYCODE_CANCEL:
+ hide();
+ break;
+ case Keyboard.KEYCODE_DELETE:
+ inputConnection.sendKeyEvent(
+ new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
+ inputConnection.sendKeyEvent(
+ new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
+ break;
+ default:
+ if (Character.isLetter(primaryCode)) {
+ inputConnection.commitText(String.valueOf((char) primaryCode), 1);
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onText(CharSequence text) {
+ if (DEBUG) {
+ Log.v(TAG, "onText clientId=" + clientId + " text=" + text);
+ }
+ if (inputConnection == null) {
+ return;
+ }
+ inputConnection.commitText(text, 0);
+ }
+ });
+ }
+}
diff --git a/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindowManager.java b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindowManager.java
new file mode 100644
index 000000000..f97c44980
--- /dev/null
+++ b/samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindowManager.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 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 com.example.android.multiclientinputmethod;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
+import android.os.IBinder;
+import android.util.SparseArray;
+import android.view.Display;
+
+final class SoftInputWindowManager {
+ private final Context mContext;
+ private final MultiClientInputMethodServiceDelegate mDelegate;
+ private final SparseArray mSoftInputWindows = new SparseArray<>();
+
+ SoftInputWindowManager(Context context, MultiClientInputMethodServiceDelegate delegate) {
+ mContext = context;
+ mDelegate = delegate;
+ }
+
+ SoftInputWindow getOrCreateSoftInputWindow(int displayId) {
+ final SoftInputWindow existingWindow = mSoftInputWindows.get(displayId);
+ if (existingWindow != null) {
+ return existingWindow;
+ }
+
+ final Display display =
+ mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
+ if (display == null) {
+ return null;
+ }
+ final IBinder windowToken = mDelegate.createInputMethodWindowToken(displayId);
+ if (windowToken == null) {
+ return null;
+ }
+
+ final Context displayContext = mContext.createDisplayContext(display);
+ final SoftInputWindow newWindow = new SoftInputWindow(displayContext, windowToken);
+ mSoftInputWindows.put(displayId, newWindow);
+ return newWindow;
+ }
+
+ SoftInputWindow getSoftInputWindow(int displayId) {
+ return mSoftInputWindows.get(displayId);
+ }
+}