From eb0868906ca7771bdd25bf61f5ee1ae0f3024fdd Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Mon, 12 Nov 2018 15:09:07 -0800 Subject: [PATCH] Add a sample multi-client IME This CL adds a sample multi-client IME. Ahthough there are still many limitations and missing features in this sample IME, this CL demonstrates how a multi-client IME can be implemented. Fix: 115784148 Test: prebuilts/checkstyle/checkstyle.py -f \ development/samples/MultiClientInputMethod/ Test: Manually verified as follows: 1. make -j MultiClientInputMethod 2. adb install -r $OUT/system/priv-app/MultiClientInputMethod/MultiClientInputMethod.apk 3. adb root 4. adb shell setprop persist.debug.multi_client_ime \ com.example.android.multiclientinputmethod/.MultiClientInputMethod 5. adb reboot 6. Try multiple text input scenario Change-Id: Ide43e16448fa00355a2c08bc45ae94d98553da50 --- samples/MultiClientInputMethod/Android.mk | 32 +++ .../AndroidManifest.xml | 30 ++ .../res/drawable/sym_keyboard_delete.png | Bin 0 -> 885 bytes .../res/drawable/sym_keyboard_done.png | Bin 0 -> 1593 bytes .../res/drawable/sym_keyboard_return.png | Bin 0 -> 536 bytes .../res/drawable/sym_keyboard_search.png | Bin 0 -> 1623 bytes .../res/drawable/sym_keyboard_shift.png | Bin 0 -> 1247 bytes .../res/drawable/sym_keyboard_space.png | Bin 0 -> 859 bytes .../res/layout/input.xml | 25 ++ .../MultiClientInputMethod/res/xml/method.xml | 17 ++ .../MultiClientInputMethod/res/xml/qwerty.xml | 79 ++++++ .../ClientCallbackImpl.java | 258 ++++++++++++++++++ .../InputMethodDebug.java | 122 +++++++++ .../MultiClientInputMethod.java | 95 +++++++ .../NoopKeyboardActionListener.java | 56 ++++ .../SoftInputWindow.java | 151 ++++++++++ .../SoftInputWindowManager.java | 61 +++++ 17 files changed, 926 insertions(+) create mode 100755 samples/MultiClientInputMethod/Android.mk create mode 100755 samples/MultiClientInputMethod/AndroidManifest.xml create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_delete.png create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_done.png create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_return.png create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_search.png create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_shift.png create mode 100755 samples/MultiClientInputMethod/res/drawable/sym_keyboard_space.png create mode 100755 samples/MultiClientInputMethod/res/layout/input.xml create mode 100644 samples/MultiClientInputMethod/res/xml/method.xml create mode 100755 samples/MultiClientInputMethod/res/xml/qwerty.xml create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/ClientCallbackImpl.java create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/InputMethodDebug.java create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/MultiClientInputMethod.java create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/NoopKeyboardActionListener.java create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindow.java create mode 100644 samples/MultiClientInputMethod/src/com/example/android/multiclientinputmethod/SoftInputWindowManager.java 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 0000000000000000000000000000000000000000..5139c7179066ca30e114eebb8dd721262dc55ec6 GIT binary patch literal 885 zcmV-*1B(2KP)o@rHU%e1hiC*e=tH zAPD$!k?>qRUL{lrPYAOipXvU}RtN{heGlOq+M|T~`Um-Z-h1Y{2qy?Lgja-)2wxt^ zMpDZm=c;W42;Sk2sy*k|G;X7#XFcR}kYv`IIBEM}!}Sw4F(RA$-=-Eiawx0kKYukcllh z^uveu4Gd^TBgkYvAkw?hzc)GlaDlJ~q0Gb<9s7AF>Iz9v5>xijmmGgQk9wen8r3I@ z<$yUJLhzGDjBD9vKIDoRT>*v5nV8va7{Ov(Vv5|P*ra1fd*T`syo9Kf_Sl!0A~%Fi zGf2Pt8X#A|Ka_S|?c)^RZP1l|=vfZY)EkqSzWFO5LE@b6q>oz|@>xs+T=E+yAQ`zyczi=?*yWu58G00000 LNkvXXu0mjf{Ku#U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..471c5021b88acefdbdabd722c10e9761c54eb40f GIT binary patch literal 1593 zcmV-92FCe`P)lyY887_q_-Fr7J=}FG^_S|7Q-<&z$oH?_c-|tslL{Y0Q zs6>H9fklBufklBufkm=#5|BJ-9B3?PjO2Yd3Q3y|K=(l(r2(2XqqwinIaoklAXrzGb)D9~^-ABSgqp$H&Kqv$C?T1BP@^EsHz>^s2+* zc<7H%>F@8q4;TtTO>CT{SS*%ue*_EhTxMqG+Zak5kKlNt(P&^@I6FJLT3%lMwaevN zg|FT)BfPlW+}!Nm-ri4ha&ia|RSby1U@##89{xibssZ#dq9`gV`oin=&cLG!)Q1cd zF2J--r*osCqCzt>Gt+oPkxwwFS&y9%0JQ-hYY0q`Yzr|AV+zcBCG8?J1`Qef?(VMZ zv@MK3V`lczQ&Lc|7@&Hz?SOjl*`Sbp8^b|lz#hQr^U9TZkXbar`p%u4ocz*evo*ux z72Nj{6||c|*;5I)jQsrk_gIzz+dH6XlC~r($(fj#=%(#PMn=8|TRu?*?b*0e&KxuO zPVD4;D7hl1(P)l-ga>&}c9oJIBSD*5R#tWc*r<1EYUJ z3rvlHP5$^5)C`OPlY(}Ggb$tyEL1o9nSG5ua|+uPJ&zG)(gRav0c(8@to0c@Tj@+I zXg5SzG0KV&%8DE|dJ&A*=I7_Tj@-g=x8P$6bVJgi4l@KJ+O|RaJF|E|viO3z(3LO0r`1*x1+)3~P0Q?Pa1vLqp$%dIE++<8A2UCDKoT z{;ki274>>OUH>YrR;#0Sold6@mBEsdk}l&i4Aj5+yrn??DJNAfK)GwDv9a-XLqh`( zlO(|NxAYv<+}wP(uCDG998&dyAOy9c!kJ2d!MmH9n!cf=5}^Mb$+f!VKu zSee9Av>>TJnlKw6;+C*nB7O!Nzdj_7{}2E@XtMopOX`KBZ*yK=Ug^@((w(zd@dWFn zrlwk!mzUR=ANNa20|#`qApO&K`#2H7E*;9IT4plTlKvAJP#oIKKs;$@EyY8shJEXp ztGl|oKCY~+6kIOXUtw6K(q@5lbaZ@p?8OrdWCHwjhI}56$1RGYU#&jev{fY~C0Ewg z*1n3QiwL*d?V+AlqsJbvtgNh0Pft%o>g|ft>6~9(U44YHcobS>&8DZMq`V3H#$cq? z?&s#_eutd$UBFX!A+hLl^)x07!w|h z6#*|7o*kIX9y>gUwzs#J!v0wV+sB{v_4N~ljwLq=3k$6dhhw0ore=W2 zSY(Pmm>oHkEgisM?@~G*7#QdS+jXoFz6v?$&C$`(-{^bDPk$u2DtpB$%Xnl4#^Y9T zb$EEVmk!=$vpq~mNcbo@Ir$R+{Rg$%?e<}l$y5aGUtm{HI0B>DKRN7`_VxAMXFRjM zxVX4MzYh)$4nZ4IsL8}SiaMNuNx{9|)6?^dG+XHI?*5qo$4o&dE!>7HFgBAR<+N)* zflyGLot^i<_8Pv64^)koV?3P9bT0)*t5s=jZEZKDSr z1<%~X^wgl##FWaylc_cg42-)xT^vIyZYBNs|KI*_0D}*YGfR(E$(4nR`D9C9tZ;Uh zwR&|Su$_el!A7!U=E6UZ9p11V ziDwJG!p-=RMa8|TK~+GUcbmy7z9TsiM|wGz1xhV-jA@uDQRwA-g{R-Od09ml)1zdE zJtYR#S42CWJ3C)a-e4sna#y-ArNQxWK&-+&rXzEjcXCZ-d-T>p$7^|v|1@XEK5K?j zovn>5d>{8Ov6><;byv%zD>P>2M_J8Yp@6nX_OlLKI8F*E^c_{KZ;xnix^#{!qnl0s z0drDR&lXulvscR*3l#*}j4mCR_`S07Vf(p<~}`x`7cI{bNCCkh1!K4Ly%&c55( z@qwv;HS@>dB@^Vf)Gk|oVIt3k58m3gGF~SqoHJFJwlF;Ow1t&8I12iZVuhWVfy01d YOPJfP!uqsUU{o`By85}Sb4q9e04T=P^Z)<= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e72cde3bb6b1265464a78f0deacaf2143bb526c2 GIT binary patch literal 1623 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 literal 0 HcmV?d00001 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); + } +}