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); + } +}