blob: 31c948a14698ff9f70f623e840287f4954f5fb7e [file] [log] [blame]
/*
* 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 android.inputmethodservice;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.IMultiClientInputMethodSession;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.view.IInputContext;
import com.android.internal.view.IInputMethodSession;
import com.android.internal.view.InputConnectionWrapper;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Re-dispatches all the incoming per-client events to the specified {@link Looper} thread.
*
* <p>There are three types of per-client callbacks.</p>
*
* <ul>
* <li>{@link IInputMethodSession} - from the IME client</li>
* <li>{@link IMultiClientInputMethodSession} - from MultiClientInputMethodManagerService</li>
* <li>{@link InputChannel} - from the IME client</li>
* </ul>
*
* <p>This class serializes all the incoming events among those channels onto
* {@link MultiClientInputMethodServiceDelegate.ClientCallback} on the specified {@link Looper}
* thread.</p>
*/
final class MultiClientInputMethodClientCallbackAdaptor {
static final boolean DEBUG = false;
static final String TAG = MultiClientInputMethodClientCallbackAdaptor.class.getSimpleName();
private final Object mSessionLock = new Object();
@GuardedBy("mSessionLock")
CallbackImpl mCallbackImpl;
@GuardedBy("mSessionLock")
InputChannel mReadChannel;
@GuardedBy("mSessionLock")
KeyEvent.DispatcherState mDispatcherState;
@GuardedBy("mSessionLock")
Handler mHandler;
@GuardedBy("mSessionLock")
@Nullable
InputEventReceiver mInputEventReceiver;
private final AtomicBoolean mFinished = new AtomicBoolean(false);
IInputMethodSession.Stub createIInputMethodSession() {
synchronized (mSessionLock) {
return new InputMethodSessionImpl(
mSessionLock, mCallbackImpl, mHandler, mFinished);
}
}
IMultiClientInputMethodSession.Stub createIMultiClientInputMethodSession() {
synchronized (mSessionLock) {
return new MultiClientInputMethodSessionImpl(
mSessionLock, mCallbackImpl, mHandler, mFinished);
}
}
MultiClientInputMethodClientCallbackAdaptor(
MultiClientInputMethodServiceDelegate.ClientCallback clientCallback, Looper looper,
KeyEvent.DispatcherState dispatcherState, InputChannel readChannel) {
synchronized (mSessionLock) {
mCallbackImpl = new CallbackImpl(this, clientCallback);
mDispatcherState = dispatcherState;
mHandler = new Handler(looper, null, true);
mReadChannel = readChannel;
mInputEventReceiver = new ImeInputEventReceiver(mReadChannel, mHandler.getLooper(),
mFinished, mDispatcherState, mCallbackImpl.mOriginalCallback);
}
}
private static final class KeyEventCallbackAdaptor implements KeyEvent.Callback {
private final MultiClientInputMethodServiceDelegate.ClientCallback mLocalCallback;
KeyEventCallbackAdaptor(
MultiClientInputMethodServiceDelegate.ClientCallback callback) {
mLocalCallback = callback;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mLocalCallback.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return mLocalCallback.onKeyLongPress(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mLocalCallback.onKeyUp(keyCode, event);
}
@Override
public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
return mLocalCallback.onKeyMultiple(keyCode, event);
}
}
private static final class ImeInputEventReceiver extends InputEventReceiver {
private final AtomicBoolean mFinished;
private final KeyEvent.DispatcherState mDispatcherState;
private final MultiClientInputMethodServiceDelegate.ClientCallback mClientCallback;
private final KeyEventCallbackAdaptor mKeyEventCallbackAdaptor;
ImeInputEventReceiver(InputChannel readChannel, Looper looper, AtomicBoolean finished,
KeyEvent.DispatcherState dispatcherState,
MultiClientInputMethodServiceDelegate.ClientCallback callback) {
super(readChannel, looper);
mFinished = finished;
mDispatcherState = dispatcherState;
mClientCallback = callback;
mKeyEventCallbackAdaptor = new KeyEventCallbackAdaptor(callback);
}
@Override
public void onInputEvent(InputEvent event) {
if (mFinished.get()) {
// The session has been finished.
finishInputEvent(event, false);
return;
}
boolean handled = false;
try {
if (event instanceof KeyEvent) {
final KeyEvent keyEvent = (KeyEvent) event;
handled = keyEvent.dispatch(mKeyEventCallbackAdaptor, mDispatcherState,
mKeyEventCallbackAdaptor);
} else {
final MotionEvent motionEvent = (MotionEvent) event;
if (motionEvent.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) {
handled = mClientCallback.onTrackballEvent(motionEvent);
} else {
handled = mClientCallback.onGenericMotionEvent(motionEvent);
}
}
} finally {
finishInputEvent(event, handled);
}
}
}
private static final class InputMethodSessionImpl extends IInputMethodSession.Stub {
private final Object mSessionLock;
@GuardedBy("mSessionLock")
private CallbackImpl mCallbackImpl;
@GuardedBy("mSessionLock")
private Handler mHandler;
private final AtomicBoolean mSessionFinished;
InputMethodSessionImpl(Object lock, CallbackImpl callback, Handler handler,
AtomicBoolean sessionFinished) {
mSessionLock = lock;
mCallbackImpl = callback;
mHandler = handler;
mSessionFinished = sessionFinished;
}
@Override
public void updateExtractedText(int token, ExtractedText text) {
reportNotSupported();
}
@Override
public void updateSelection(int oldSelStart, int oldSelEnd,
int newSelStart, int newSelEnd,
int candidatesStart, int candidatesEnd) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
final SomeArgs args = SomeArgs.obtain();
args.argi1 = oldSelStart;
args.argi2 = oldSelEnd;
args.argi3 = newSelStart;
args.argi4 = newSelEnd;
args.argi5 = candidatesStart;
args.argi6 = candidatesEnd;
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::updateSelection, mCallbackImpl, args));
}
}
@Override
public void viewClicked(boolean focusChanged) {
reportNotSupported();
}
@Override
public void updateCursor(Rect newCursor) {
reportNotSupported();
}
@Override
public void displayCompletions(CompletionInfo[] completions) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::displayCompletions, mCallbackImpl, completions));
}
}
@Override
public void appPrivateCommand(String action, Bundle data) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::appPrivateCommand, mCallbackImpl, action, data));
}
}
@Override
public void toggleSoftInput(int showFlags, int hideFlags) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::toggleSoftInput, mCallbackImpl, showFlags,
hideFlags));
}
}
@Override
public void finishSession() {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mSessionFinished.set(true);
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::finishSession, mCallbackImpl));
mCallbackImpl = null;
mHandler = null;
}
}
@Override
public void updateCursorAnchorInfo(CursorAnchorInfo info) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::updateCursorAnchorInfo, mCallbackImpl, info));
}
}
@Override
public final void notifyImeHidden() {
// no-op for multi-session since IME is responsible controlling navigation bar buttons.
reportNotSupported();
}
}
private static final class MultiClientInputMethodSessionImpl
extends IMultiClientInputMethodSession.Stub {
private final Object mSessionLock;
@GuardedBy("mSessionLock")
private CallbackImpl mCallbackImpl;
@GuardedBy("mSessionLock")
private Handler mHandler;
private final AtomicBoolean mSessionFinished;
MultiClientInputMethodSessionImpl(Object lock, CallbackImpl callback,
Handler handler, AtomicBoolean sessionFinished) {
mSessionLock = lock;
mCallbackImpl = callback;
mHandler = handler;
mSessionFinished = sessionFinished;
}
@Override
public void startInputOrWindowGainedFocus(@Nullable IInputContext inputContext,
int missingMethods, @Nullable EditorInfo editorInfo, int controlFlags,
@SoftInputModeFlags int softInputMode, int windowHandle) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
final SomeArgs args = SomeArgs.obtain();
// TODO(Bug 119211536): Remove dependency on AbstractInputMethodService from ICW
final WeakReference<AbstractInputMethodService> fakeIMS =
new WeakReference<>(null);
args.arg1 = (inputContext == null) ? null
: new InputConnectionWrapper(fakeIMS, inputContext, missingMethods,
mSessionFinished);
args.arg2 = editorInfo;
args.argi1 = controlFlags;
args.argi2 = softInputMode;
args.argi3 = windowHandle;
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::startInputOrWindowGainedFocus, mCallbackImpl, args));
}
}
@Override
public void showSoftInput(int flags, ResultReceiver resultReceiver) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::showSoftInput, mCallbackImpl, flags,
resultReceiver));
}
}
@Override
public void hideSoftInput(int flags, ResultReceiver resultReceiver) {
synchronized (mSessionLock) {
if (mCallbackImpl == null || mHandler == null) {
return;
}
mHandler.sendMessage(PooledLambda.obtainMessage(
CallbackImpl::hideSoftInput, mCallbackImpl, flags,
resultReceiver));
}
}
}
/**
* The maim part of adaptor to {@link MultiClientInputMethodServiceDelegate.ClientCallback}.
*/
@WorkerThread
private static final class CallbackImpl {
private final MultiClientInputMethodClientCallbackAdaptor mCallbackAdaptor;
private final MultiClientInputMethodServiceDelegate.ClientCallback mOriginalCallback;
private boolean mFinished = false;
CallbackImpl(MultiClientInputMethodClientCallbackAdaptor callbackAdaptor,
MultiClientInputMethodServiceDelegate.ClientCallback callback) {
mCallbackAdaptor = callbackAdaptor;
mOriginalCallback = callback;
}
void updateSelection(SomeArgs args) {
try {
if (mFinished) {
return;
}
mOriginalCallback.onUpdateSelection(args.argi1, args.argi2, args.argi3,
args.argi4, args.argi5, args.argi6);
} finally {
args.recycle();
}
}
void displayCompletions(CompletionInfo[] completions) {
if (mFinished) {
return;
}
mOriginalCallback.onDisplayCompletions(completions);
}
void appPrivateCommand(String action, Bundle data) {
if (mFinished) {
return;
}
mOriginalCallback.onAppPrivateCommand(action, data);
}
void toggleSoftInput(int showFlags, int hideFlags) {
if (mFinished) {
return;
}
mOriginalCallback.onToggleSoftInput(showFlags, hideFlags);
}
void finishSession() {
if (mFinished) {
return;
}
mFinished = true;
mOriginalCallback.onFinishSession();
synchronized (mCallbackAdaptor.mSessionLock) {
mCallbackAdaptor.mDispatcherState = null;
if (mCallbackAdaptor.mReadChannel != null) {
mCallbackAdaptor.mReadChannel.dispose();
mCallbackAdaptor.mReadChannel = null;
}
mCallbackAdaptor.mInputEventReceiver = null;
}
}
void updateCursorAnchorInfo(CursorAnchorInfo info) {
if (mFinished) {
return;
}
mOriginalCallback.onUpdateCursorAnchorInfo(info);
}
void startInputOrWindowGainedFocus(SomeArgs args) {
try {
if (mFinished) {
return;
}
final InputConnectionWrapper inputConnection = (InputConnectionWrapper) args.arg1;
final EditorInfo editorInfo = (EditorInfo) args.arg2;
final int startInputFlags = args.argi1;
final int softInputMode = args.argi2;
final int windowHandle = args.argi3;
mOriginalCallback.onStartInputOrWindowGainedFocus(inputConnection, editorInfo,
startInputFlags, softInputMode, windowHandle);
} finally {
args.recycle();
}
}
void showSoftInput(int flags, ResultReceiver resultReceiver) {
if (mFinished) {
return;
}
mOriginalCallback.onShowSoftInput(flags, resultReceiver);
}
void hideSoftInput(int flags, ResultReceiver resultReceiver) {
if (mFinished) {
return;
}
mOriginalCallback.onHideSoftInput(flags, resultReceiver);
}
}
private static void reportNotSupported() {
if (DEBUG) {
Log.d(TAG, Debug.getCaller() + " is not supported");
}
}
}