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
This commit is contained in:
Yohei Yukawa
2018-11-12 15:09:07 -08:00
parent ceb1c20000
commit eb0868906c
17 changed files with 926 additions and 0 deletions

View File

@@ -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)

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.multiclientinputmethod">
<application android:label="MultiClientInputMethod">
<service android:name=".MultiClientInputMethod"
android:label="MultiClientInputMethod"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.inputmethodservice.MultiClientInputMethodService"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.KeyboardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<input-method xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p"
android:horizontalGap="0px"
android:verticalGap="0px"
android:keyHeight="50dip"
>
<Row>
<Key android:codes="113" android:keyLabel="q" android:keyEdgeFlags="left"/>
<Key android:codes="119" android:keyLabel="w"/>
<Key android:codes="101" android:keyLabel="e"/>
<Key android:codes="114" android:keyLabel="r"/>
<Key android:codes="116" android:keyLabel="t"/>
<Key android:codes="121" android:keyLabel="y"/>
<Key android:codes="117" android:keyLabel="u"/>
<Key android:codes="105" android:keyLabel="i"/>
<Key android:codes="111" android:keyLabel="o"/>
<Key android:codes="112" android:keyLabel="p" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="97" android:keyLabel="a" android:horizontalGap="5%p"
android:keyEdgeFlags="left"/>
<Key android:codes="115" android:keyLabel="s"/>
<Key android:codes="100" android:keyLabel="d"/>
<Key android:codes="102" android:keyLabel="f"/>
<Key android:codes="103" android:keyLabel="g"/>
<Key android:codes="104" android:keyLabel="h"/>
<Key android:codes="106" android:keyLabel="j"/>
<Key android:codes="107" android:keyLabel="k"/>
<Key android:codes="108" android:keyLabel="l" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="-1" android:keyIcon="@drawable/sym_keyboard_shift"
android:keyWidth="15%p" android:isModifier="true"
android:isSticky="true" android:keyEdgeFlags="left"/>
<Key android:codes="122" android:keyLabel="z"/>
<Key android:codes="120" android:keyLabel="x"/>
<Key android:codes="99" android:keyLabel="c"/>
<Key android:codes="118" android:keyLabel="v"/>
<Key android:codes="98" android:keyLabel="b"/>
<Key android:codes="110" android:keyLabel="n"/>
<Key android:codes="109" android:keyLabel="m"/>
<Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete"
android:keyWidth="15%p" android:keyEdgeFlags="right"
android:isRepeatable="true"/>
</Row>
<Row android:rowEdgeFlags="bottom">
<Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done"
android:keyWidth="20%p" android:keyEdgeFlags="left"/>
<Key android:codes="-2" android:keyLabel="123" android:keyWidth="10%p"/>
<Key android:codes="32" android:keyIcon="@drawable/sym_keyboard_space"
android:keyWidth="40%p" android:isRepeatable="true"/>
<Key android:codes="46,44" android:keyLabel=". ,"
android:keyWidth="20%p"/>
<Key android:codes="10" android:keyIcon="@drawable/sym_keyboard_return"
android:keyWidth="20%p" android:keyEdgeFlags="right"/>
</Row>
</Keyboard>

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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() {
}
}

View File

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

View File

@@ -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<SoftInputWindow> 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);
}
}