/*
|
* 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.android.server.inputmethod;
|
|
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
|
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
|
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
|
import android.annotation.AnyThread;
|
import android.annotation.BinderThread;
|
import android.annotation.IntDef;
|
import android.annotation.MainThread;
|
import android.annotation.Nullable;
|
import android.annotation.UserIdInt;
|
import android.annotation.WorkerThread;
|
import android.app.AppOpsManager;
|
import android.app.Notification;
|
import android.app.NotificationManager;
|
import android.app.PendingIntent;
|
import android.content.BroadcastReceiver;
|
import android.content.ComponentName;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.IntentFilter;
|
import android.content.ServiceConnection;
|
import android.content.pm.ApplicationInfo;
|
import android.content.pm.PackageManager;
|
import android.content.pm.ResolveInfo;
|
import android.content.pm.ServiceInfo;
|
import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
|
import android.net.Uri;
|
import android.os.Binder;
|
import android.os.Build;
|
import android.os.Bundle;
|
import android.os.Debug;
|
import android.os.Handler;
|
import android.os.HandlerThread;
|
import android.os.IBinder;
|
import android.os.RemoteException;
|
import android.os.ResultReceiver;
|
import android.os.ShellCallback;
|
import android.os.UserHandle;
|
import android.provider.Settings;
|
import android.text.TextUtils;
|
import android.util.ArrayMap;
|
import android.util.ArraySet;
|
import android.util.Slog;
|
import android.util.SparseArray;
|
import android.view.InputChannel;
|
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
|
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
|
import android.view.inputmethod.InputMethodInfo;
|
import android.view.inputmethod.InputMethodSubtype;
|
import android.view.inputmethod.InputMethodSystemProperty;
|
|
import com.android.internal.R;
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.inputmethod.IMultiClientInputMethod;
|
import com.android.internal.inputmethod.IMultiClientInputMethodPrivilegedOperations;
|
import com.android.internal.inputmethod.IMultiClientInputMethodSession;
|
import com.android.internal.inputmethod.StartInputFlags;
|
import com.android.internal.inputmethod.StartInputReason;
|
import com.android.internal.inputmethod.UnbindReason;
|
import com.android.internal.messages.nano.SystemMessageProto;
|
import com.android.internal.notification.SystemNotificationChannels;
|
import com.android.internal.os.TransferPipe;
|
import com.android.internal.util.DumpUtils;
|
import com.android.internal.util.IndentingPrintWriter;
|
import com.android.internal.util.function.pooled.PooledLambda;
|
import com.android.internal.view.IInputContext;
|
import com.android.internal.view.IInputMethodClient;
|
import com.android.internal.view.IInputMethodManager;
|
import com.android.internal.view.IInputMethodSession;
|
import com.android.internal.view.InputBindResult;
|
import com.android.server.LocalServices;
|
import com.android.server.SystemService;
|
import com.android.server.wm.WindowManagerInternal;
|
|
import java.io.FileDescriptor;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.lang.annotation.Retention;
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.WeakHashMap;
|
|
/**
|
* Actual implementation of multi-client InputMethodManagerService.
|
*
|
* <p>This system service is intentionally compatible with {@link InputMethodManagerService} so that
|
* we can switch the implementation at the boot time.</p>
|
*/
|
public final class MultiClientInputMethodManagerService {
|
private static final String TAG = "MultiClientInputMethodManagerService";
|
private static final boolean DEBUG = false;
|
|
private static final String PER_DISPLAY_FOCUS_DISABLED_WARNING_TITLE =
|
"config_perDisplayFocusEnabled is not true.";
|
|
private static final String PER_DISPLAY_FOCUS_DISABLED_WARNING_MSG =
|
"Consider rebuilding the system image after enabling config_perDisplayFocusEnabled to "
|
+ "make IME focus compatible with multi-client IME mode.";
|
|
private static final long RECONNECT_DELAY_MSEC = 1000;
|
|
/**
|
* Unlike {@link InputMethodManagerService}, {@link MultiClientInputMethodManagerService}
|
* always binds to the IME with {@link Context#BIND_FOREGROUND_SERVICE} for now for simplicity.
|
*/
|
private static final int IME_CONNECTION_UNIFIED_BIND_FLAGS =
|
Context.BIND_AUTO_CREATE
|
| Context.BIND_NOT_VISIBLE
|
| Context.BIND_NOT_FOREGROUND
|
| Context.BIND_FOREGROUND_SERVICE;
|
|
private static final ComponentName sImeComponentName =
|
InputMethodSystemProperty.sMultiClientImeComponentName;
|
|
private static void reportNotSupported() {
|
if (DEBUG) {
|
Slog.d(TAG, "non-supported operation. callers=" + Debug.getCallers(3));
|
}
|
}
|
|
/**
|
* {@link MultiClientInputMethodManagerService} is not intended to be instantiated.
|
*/
|
private MultiClientInputMethodManagerService() {
|
}
|
|
/**
|
* The implementation of {@link SystemService} for multi-client IME.
|
*/
|
public static final class Lifecycle extends SystemService {
|
private final ApiCallbacks mApiCallbacks;
|
private final OnWorkerThreadCallback mOnWorkerThreadCallback;
|
|
@MainThread
|
public Lifecycle(Context context) {
|
super(context);
|
|
final UserToInputMethodInfoMap userIdToInputMethodInfoMapper =
|
new UserToInputMethodInfoMap();
|
final UserDataMap userDataMap = new UserDataMap();
|
final HandlerThread workerThread = new HandlerThread(TAG);
|
workerThread.start();
|
mApiCallbacks = new ApiCallbacks(context, userDataMap, userIdToInputMethodInfoMapper);
|
mOnWorkerThreadCallback = new OnWorkerThreadCallback(
|
context, userDataMap, userIdToInputMethodInfoMapper,
|
new Handler(workerThread.getLooper(), msg -> false, true));
|
|
LocalServices.addService(InputMethodManagerInternal.class,
|
new InputMethodManagerInternal() {
|
@Override
|
public void setInteractive(boolean interactive) {
|
reportNotSupported();
|
}
|
|
@Override
|
public void hideCurrentInputMethod() {
|
reportNotSupported();
|
}
|
|
@Override
|
public List<InputMethodInfo> getInputMethodListAsUser(
|
@UserIdInt int userId) {
|
return userIdToInputMethodInfoMapper.getAsList(userId);
|
}
|
|
@Override
|
public List<InputMethodInfo> getEnabledInputMethodListAsUser(
|
@UserIdInt int userId) {
|
return userIdToInputMethodInfoMapper.getAsList(userId);
|
}
|
});
|
}
|
|
@MainThread
|
@Override
|
public void onBootPhase(int phase) {
|
mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
|
OnWorkerThreadCallback::onBootPhase, mOnWorkerThreadCallback, phase));
|
}
|
|
@MainThread
|
@Override
|
public void onStart() {
|
publishBinderService(Context.INPUT_METHOD_SERVICE, mApiCallbacks);
|
}
|
|
@MainThread
|
@Override
|
public void onStartUser(@UserIdInt int userId) {
|
mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
|
OnWorkerThreadCallback::onStartUser, mOnWorkerThreadCallback, userId));
|
}
|
|
@MainThread
|
@Override
|
public void onUnlockUser(@UserIdInt int userId) {
|
mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
|
OnWorkerThreadCallback::onUnlockUser, mOnWorkerThreadCallback, userId));
|
}
|
|
@MainThread
|
@Override
|
public void onStopUser(@UserIdInt int userId) {
|
mOnWorkerThreadCallback.getHandler().sendMessage(PooledLambda.obtainMessage(
|
OnWorkerThreadCallback::onStopUser, mOnWorkerThreadCallback, userId));
|
}
|
}
|
|
private static final class OnWorkerThreadCallback {
|
private final Context mContext;
|
private final UserDataMap mUserDataMap;
|
private final UserToInputMethodInfoMap mInputMethodInfoMap;
|
private final Handler mHandler;
|
|
OnWorkerThreadCallback(Context context, UserDataMap userDataMap,
|
UserToInputMethodInfoMap inputMethodInfoMap, Handler handler) {
|
mContext = context;
|
mUserDataMap = userDataMap;
|
mInputMethodInfoMap = inputMethodInfoMap;
|
mHandler = handler;
|
}
|
|
@AnyThread
|
Handler getHandler() {
|
return mHandler;
|
}
|
|
@WorkerThread
|
private void tryBindInputMethodService(@UserIdInt int userId) {
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.i(TAG, "tryBindInputMethodService is called for an unknown user=" + userId);
|
return;
|
}
|
|
final InputMethodInfo imi = queryInputMethod(mContext, userId, sImeComponentName);
|
if (imi == null) {
|
Slog.w(TAG, "Multi-client InputMethod is not found. component="
|
+ sImeComponentName);
|
synchronized (data.mLock) {
|
switch (data.mState) {
|
case PerUserState.USER_LOCKED:
|
case PerUserState.SERVICE_NOT_QUERIED:
|
case PerUserState.SERVICE_RECOGNIZED:
|
case PerUserState.UNBIND_CALLED:
|
// Safe to clean up.
|
mInputMethodInfoMap.remove(userId);
|
break;
|
}
|
}
|
return;
|
}
|
|
synchronized (data.mLock) {
|
switch (data.mState) {
|
case PerUserState.USER_LOCKED:
|
// If the user is still locked, we currently do not try to start IME.
|
return;
|
case PerUserState.SERVICE_NOT_QUERIED:
|
case PerUserState.SERVICE_RECOGNIZED:
|
case PerUserState.UNBIND_CALLED:
|
break;
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
case PerUserState.SERVICE_CONNECTED:
|
// OK, nothing to do.
|
return;
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
return;
|
}
|
data.mState = PerUserState.SERVICE_RECOGNIZED;
|
data.mCurrentInputMethodInfo = imi;
|
mInputMethodInfoMap.put(userId, imi);
|
final boolean bindResult = data.bindServiceLocked(mContext, userId);
|
if (!bindResult) {
|
Slog.e(TAG, "Failed to bind Multi-client InputMethod.");
|
return;
|
}
|
data.mState = PerUserState.WAITING_SERVICE_CONNECTED;
|
}
|
}
|
|
@WorkerThread
|
void onStartUser(@UserIdInt int userId) {
|
if (DEBUG) {
|
Slog.v(TAG, "onStartUser userId=" + userId);
|
}
|
final PerUserData data = new PerUserData(userId, null, PerUserState.USER_LOCKED, this);
|
mUserDataMap.put(userId, data);
|
}
|
|
@WorkerThread
|
void onUnlockUser(@UserIdInt int userId) {
|
if (DEBUG) {
|
Slog.v(TAG, "onUnlockUser() userId=" + userId);
|
}
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.i(TAG, "onUnlockUser is called for an unknown user=" + userId);
|
return;
|
}
|
synchronized (data.mLock) {
|
switch (data.mState) {
|
case PerUserState.USER_LOCKED:
|
data.mState = PerUserState.SERVICE_NOT_QUERIED;
|
tryBindInputMethodService(userId);
|
break;
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
break;
|
}
|
}
|
}
|
|
@WorkerThread
|
void onStopUser(@UserIdInt int userId) {
|
if (DEBUG) {
|
Slog.v(TAG, "onStopUser() userId=" + userId);
|
}
|
mInputMethodInfoMap.remove(userId);
|
final PerUserData data = mUserDataMap.removeReturnOld(userId);
|
if (data == null) {
|
Slog.i(TAG, "onStopUser is called for an unknown user=" + userId);
|
return;
|
}
|
synchronized (data.mLock) {
|
switch (data.mState) {
|
case PerUserState.USER_LOCKED:
|
case PerUserState.SERVICE_RECOGNIZED:
|
case PerUserState.UNBIND_CALLED:
|
// OK, nothing to do.
|
return;
|
case PerUserState.SERVICE_CONNECTED:
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
break;
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
break;
|
}
|
data.unbindServiceLocked(mContext);
|
data.mState = PerUserState.UNBIND_CALLED;
|
data.mCurrentInputMethod = null;
|
|
// When a Service is explicitly unbound with Context.unbindService(),
|
// onServiceDisconnected() will not be triggered. Hence here we explicitly call
|
// onInputMethodDisconnectedLocked() as if the Service is already gone.
|
data.onInputMethodDisconnectedLocked();
|
}
|
}
|
|
@WorkerThread
|
void onServiceConnected(PerUserData data, IMultiClientInputMethod service) {
|
if (DEBUG) {
|
Slog.v(TAG, "onServiceConnected() data.mUserId=" + data.mUserId);
|
}
|
synchronized (data.mLock) {
|
switch (data.mState) {
|
case PerUserState.UNBIND_CALLED:
|
// We should ignore this callback.
|
return;
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
// OK.
|
data.mState = PerUserState.SERVICE_CONNECTED;
|
data.mCurrentInputMethod = service;
|
try {
|
data.mCurrentInputMethod.initialize(new ImeCallbacks(data));
|
} catch (RemoteException e) {
|
}
|
data.onInputMethodConnectedLocked();
|
break;
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
return;
|
}
|
}
|
}
|
|
@WorkerThread
|
void onServiceDisconnected(PerUserData data) {
|
if (DEBUG) {
|
Slog.v(TAG, "onServiceDisconnected() data.mUserId=" + data.mUserId);
|
}
|
final WindowManagerInternal windowManagerInternal =
|
LocalServices.getService(WindowManagerInternal.class);
|
synchronized (data.mLock) {
|
// We assume the number of tokens would not be that large (up to 10 or so) hence
|
// linear search should be acceptable.
|
final int numTokens = data.mDisplayIdToImeWindowTokenMap.size();
|
for (int i = 0; i < numTokens; ++i) {
|
final TokenInfo info = data.mDisplayIdToImeWindowTokenMap.valueAt(i);
|
windowManagerInternal.removeWindowToken(info.mToken, false, info.mDisplayId);
|
}
|
data.mDisplayIdToImeWindowTokenMap.clear();
|
switch (data.mState) {
|
case PerUserState.UNBIND_CALLED:
|
// We should ignore this callback.
|
return;
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
case PerUserState.SERVICE_CONNECTED:
|
// onServiceDisconnected() means the biding is still alive.
|
data.mState = PerUserState.WAITING_SERVICE_CONNECTED;
|
data.mCurrentInputMethod = null;
|
data.onInputMethodDisconnectedLocked();
|
break;
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
return;
|
}
|
}
|
}
|
|
@WorkerThread
|
void onBindingDied(PerUserData data) {
|
if (DEBUG) {
|
Slog.v(TAG, "onBindingDied() data.mUserId=" + data.mUserId);
|
}
|
final WindowManagerInternal windowManagerInternal =
|
LocalServices.getService(WindowManagerInternal.class);
|
synchronized (data.mLock) {
|
// We assume the number of tokens would not be that large (up to 10 or so) hence
|
// linear search should be acceptable.
|
final int numTokens = data.mDisplayIdToImeWindowTokenMap.size();
|
for (int i = 0; i < numTokens; ++i) {
|
final TokenInfo info = data.mDisplayIdToImeWindowTokenMap.valueAt(i);
|
windowManagerInternal.removeWindowToken(info.mToken, false, info.mDisplayId);
|
}
|
data.mDisplayIdToImeWindowTokenMap.clear();
|
switch (data.mState) {
|
case PerUserState.UNBIND_CALLED:
|
// We should ignore this callback.
|
return;
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
case PerUserState.SERVICE_CONNECTED: {
|
// onBindingDied() means the biding is dead.
|
data.mState = PerUserState.UNBIND_CALLED;
|
data.mCurrentInputMethod = null;
|
data.onInputMethodDisconnectedLocked();
|
// Schedule a retry
|
mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
|
OnWorkerThreadCallback::tryBindInputMethodService,
|
this, data.mUserId), RECONNECT_DELAY_MSEC);
|
break;
|
}
|
default:
|
Slog.wtf(TAG, "Unknown state=" + data.mState);
|
return;
|
}
|
}
|
}
|
|
@WorkerThread
|
void onBootPhase(int phase) {
|
if (DEBUG) {
|
Slog.v(TAG, "onBootPhase() phase=" + phase);
|
}
|
switch (phase) {
|
case SystemService.PHASE_ACTIVITY_MANAGER_READY: {
|
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
filter.addDataScheme("package");
|
mContext.registerReceiver(new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
onPackageAdded(intent);
|
}
|
}, filter, null, mHandler);
|
break;
|
}
|
case SystemService.PHASE_BOOT_COMPLETED: {
|
final boolean perDisplayFocusEnabled = mContext.getResources().getBoolean(
|
com.android.internal.R.bool.config_perDisplayFocusEnabled);
|
if (!perDisplayFocusEnabled) {
|
final Bundle extras = new Bundle();
|
extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
|
mContext.getSystemService(NotificationManager.class).notifyAsUser(TAG,
|
SystemMessageProto.SystemMessage.NOTE_SELECT_INPUT_METHOD,
|
new Notification.Builder(mContext,
|
SystemNotificationChannels.VIRTUAL_KEYBOARD)
|
.setContentTitle(PER_DISPLAY_FOCUS_DISABLED_WARNING_TITLE)
|
.setStyle(new Notification.BigTextStyle()
|
.bigText(PER_DISPLAY_FOCUS_DISABLED_WARNING_MSG))
|
.setSmallIcon(R.drawable.ic_notification_ime_default)
|
.setWhen(0)
|
.setOngoing(true)
|
.setLocalOnly(true)
|
.addExtras(extras)
|
.setCategory(Notification.CATEGORY_SYSTEM)
|
.setColor(mContext.getColor(
|
R.color.system_notification_accent_color))
|
.build(), UserHandle.ALL);
|
}
|
break;
|
}
|
}
|
}
|
|
@WorkerThread
|
void onPackageAdded(Intent intent) {
|
if (DEBUG) {
|
Slog.v(TAG, "onPackageAdded() intent=" + intent);
|
}
|
final Uri uri = intent.getData();
|
if (uri == null) {
|
return;
|
}
|
if (!intent.hasExtra(Intent.EXTRA_UID)) {
|
return;
|
}
|
final String packageName = uri.getSchemeSpecificPart();
|
if (sImeComponentName == null
|
|| packageName == null
|
|| !TextUtils.equals(sImeComponentName.getPackageName(), packageName)) {
|
return;
|
}
|
final int userId = UserHandle.getUserId(intent.getIntExtra(Intent.EXTRA_UID, 0));
|
tryBindInputMethodService(userId);
|
}
|
}
|
|
private static final class WindowInfo {
|
final IBinder mWindowToken;
|
final int mWindowHandle;
|
|
WindowInfo(IBinder windowToken, int windowCookie) {
|
mWindowToken = windowToken;
|
mWindowHandle = windowCookie;
|
}
|
}
|
|
/**
|
* Describes the state of each IME client.
|
*/
|
@Retention(SOURCE)
|
@IntDef({InputMethodClientState.REGISTERED,
|
InputMethodClientState.WAITING_FOR_IME_SESSION,
|
InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT,
|
InputMethodClientState.ALREADY_SENT_BIND_RESULT,
|
InputMethodClientState.UNREGISTERED})
|
private @interface InputMethodClientState {
|
/**
|
* {@link IInputMethodManager#addClient(IInputMethodClient, IInputContext, int)} is called
|
* and this client is now recognized by the system. When the system lost the connection to
|
* the current IME, all the clients need to be re-initialized from this state.
|
*/
|
int REGISTERED = 1;
|
/**
|
* This client is notified to the current IME with {@link
|
* IMultiClientInputMethod#addClient(int, int, int, int)} but the IME is not yet responded
|
* with {@link IMultiClientInputMethodPrivilegedOperations#acceptClient(int,
|
* IInputMethodSession, IMultiClientInputMethodSession, InputChannel)}.
|
*/
|
int WAITING_FOR_IME_SESSION = 2;
|
/**
|
* This client is already accepted by the IME but a valid {@link InputBindResult} has not
|
* been returned to the client yet.
|
*/
|
int READY_TO_SEND_FIRST_BIND_RESULT = 3;
|
/**
|
* This client has already received a valid {@link InputBindResult} at least once. This
|
* means that the client can directly call {@link IInputMethodSession} IPCs and key events
|
* via {@link InputChannel}. When the current IME is unbound, these client end points also
|
* need to be cleared.
|
*/
|
int ALREADY_SENT_BIND_RESULT = 4;
|
/**
|
* The client process is dying.
|
*/
|
int UNREGISTERED = 5;
|
}
|
|
private static final class InputMethodClientIdSource {
|
@GuardedBy("InputMethodClientIdSource.class")
|
private static int sNextValue = 0;
|
|
private InputMethodClientIdSource() {
|
}
|
|
static synchronized int getNext() {
|
final int result = sNextValue;
|
sNextValue++;
|
if (sNextValue < 0) {
|
sNextValue = 0;
|
}
|
return result;
|
}
|
}
|
|
private static final class WindowHandleSource {
|
@GuardedBy("WindowHandleSource.class")
|
private static int sNextValue = 0;
|
|
private WindowHandleSource() {
|
}
|
|
static synchronized int getNext() {
|
final int result = sNextValue;
|
sNextValue++;
|
if (sNextValue < 0) {
|
sNextValue = 0;
|
}
|
return result;
|
}
|
}
|
|
private static final class InputMethodClientInfo {
|
final IInputMethodClient mClient;
|
final int mUid;
|
final int mPid;
|
final int mSelfReportedDisplayId;
|
final int mClientId;
|
|
@GuardedBy("PerUserData.mLock")
|
@InputMethodClientState
|
int mState;
|
@GuardedBy("PerUserData.mLock")
|
int mBindingSequence;
|
@GuardedBy("PerUserData.mLock")
|
InputChannel mWriteChannel;
|
@GuardedBy("PerUserData.mLock")
|
IInputMethodSession mInputMethodSession;
|
@GuardedBy("PerUserData.mLock")
|
IMultiClientInputMethodSession mMSInputMethodSession;
|
@GuardedBy("PerUserData.mLock")
|
final WeakHashMap<IBinder, WindowInfo> mWindowMap = new WeakHashMap<>();
|
|
InputMethodClientInfo(IInputMethodClient client, int uid, int pid,
|
int selfReportedDisplayId) {
|
mClient = client;
|
mUid = uid;
|
mPid = pid;
|
mSelfReportedDisplayId = selfReportedDisplayId;
|
mClientId = InputMethodClientIdSource.getNext();
|
}
|
|
@GuardedBy("PerUserData.mLock")
|
void dumpLocked(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
|
ipw.println("mState=" + mState + ",mBindingSequence=" + mBindingSequence
|
+ ",mWriteChannel=" + mWriteChannel
|
+ ",mInputMethodSession=" + mInputMethodSession
|
+ ",mMSInputMethodSession=" + mMSInputMethodSession);
|
}
|
}
|
|
private static final class UserDataMap {
|
@GuardedBy("mMap")
|
private final SparseArray<PerUserData> mMap = new SparseArray<>();
|
|
@AnyThread
|
@Nullable
|
PerUserData get(@UserIdInt int userId) {
|
synchronized (mMap) {
|
return mMap.get(userId);
|
}
|
}
|
|
@AnyThread
|
void put(@UserIdInt int userId, PerUserData data) {
|
synchronized (mMap) {
|
mMap.put(userId, data);
|
}
|
}
|
|
@AnyThread
|
@Nullable
|
PerUserData removeReturnOld(@UserIdInt int userId) {
|
synchronized (mMap) {
|
return mMap.removeReturnOld(userId);
|
}
|
}
|
|
@AnyThread
|
void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
|
synchronized (mMap) {
|
for (int i = 0; i < mMap.size(); i++) {
|
int userId = mMap.keyAt(i);
|
PerUserData data = mMap.valueAt(i);
|
ipw.println("userId=" + userId + ", data=");
|
if (data != null) {
|
ipw.increaseIndent();
|
data.dump(fd, ipw, args);
|
ipw.decreaseIndent();
|
}
|
}
|
}
|
}
|
}
|
|
private static final class TokenInfo {
|
final Binder mToken;
|
final int mDisplayId;
|
TokenInfo(Binder token, int displayId) {
|
mToken = token;
|
mDisplayId = displayId;
|
}
|
}
|
|
@Retention(SOURCE)
|
@IntDef({
|
PerUserState.USER_LOCKED,
|
PerUserState.SERVICE_NOT_QUERIED,
|
PerUserState.SERVICE_RECOGNIZED,
|
PerUserState.WAITING_SERVICE_CONNECTED,
|
PerUserState.SERVICE_CONNECTED,
|
PerUserState.UNBIND_CALLED})
|
private @interface PerUserState {
|
/**
|
* The user is still locked.
|
*/
|
int USER_LOCKED = 1;
|
/**
|
* The system has not queried whether there is a multi-client IME or not.
|
*/
|
int SERVICE_NOT_QUERIED = 2;
|
/**
|
* A multi-client IME specified in {@link #PROP_DEBUG_MULTI_CLIENT_IME} is found in the
|
* system, but not bound yet.
|
*/
|
int SERVICE_RECOGNIZED = 3;
|
/**
|
* {@link Context#bindServiceAsUser(Intent, ServiceConnection, int, Handler, UserHandle)} is
|
* already called for the IME but
|
* {@link ServiceConnection#onServiceConnected(ComponentName, IBinder)} is not yet called
|
* back. This includes once the IME is bound but temporarily disconnected as notified with
|
* {@link ServiceConnection#onServiceDisconnected(ComponentName)}.
|
*/
|
int WAITING_SERVICE_CONNECTED = 4;
|
/**
|
* {@link ServiceConnection#onServiceConnected(ComponentName, IBinder)} is already called
|
* back. The IME is ready to be used.
|
*/
|
int SERVICE_CONNECTED = 5;
|
/**
|
* The binding is gone. Either {@link Context#unbindService(ServiceConnection)} is
|
* explicitly called or the system decided to destroy the binding as notified with
|
* {@link ServiceConnection#onBindingDied(ComponentName)}.
|
*/
|
int UNBIND_CALLED = 6;
|
}
|
|
/**
|
* Takes care of per-user state separation.
|
*/
|
private static final class PerUserData {
|
final Object mLock = new Object();
|
|
/**
|
* User ID (not UID) that is associated with this data.
|
*/
|
@UserIdInt
|
private final int mUserId;
|
|
/**
|
* {@link IMultiClientInputMethod} of the currently connected multi-client IME. This
|
* must be non-{@code null} only while {@link #mState} is
|
* {@link PerUserState#SERVICE_CONNECTED}.
|
*/
|
@Nullable
|
@GuardedBy("mLock")
|
IMultiClientInputMethod mCurrentInputMethod;
|
|
/**
|
* {@link InputMethodInfo} of the currently selected multi-client IME. This must be
|
* non-{@code null} unless {@link #mState} is {@link PerUserState#SERVICE_NOT_QUERIED}.
|
*/
|
@GuardedBy("mLock")
|
@Nullable
|
InputMethodInfo mCurrentInputMethodInfo;
|
|
/**
|
* Describes the current service state.
|
*/
|
@GuardedBy("mLock")
|
@PerUserState
|
int mState;
|
|
/**
|
* A {@link SparseArray} that maps display ID to IME Window token that is already issued to
|
* the IME.
|
*/
|
@GuardedBy("mLock")
|
final ArraySet<TokenInfo> mDisplayIdToImeWindowTokenMap = new ArraySet<>();
|
|
@GuardedBy("mLock")
|
private final ArrayMap<IBinder, InputMethodClientInfo> mClientMap = new ArrayMap<>();
|
|
@GuardedBy("mLock")
|
private SparseArray<InputMethodClientInfo> mClientIdToClientMap = new SparseArray<>();
|
|
private final OnWorkerThreadServiceConnection mOnWorkerThreadServiceConnection;
|
|
/**
|
* A {@link ServiceConnection} that is designed to run on a certain worker thread with
|
* which {@link OnWorkerThreadCallback} is associated.
|
*
|
* @see Context#bindServiceAsUser(Intent, ServiceConnection, int, Handler, UserHandle).
|
*/
|
private static final class OnWorkerThreadServiceConnection implements ServiceConnection {
|
private final PerUserData mData;
|
private final OnWorkerThreadCallback mCallback;
|
|
OnWorkerThreadServiceConnection(PerUserData data, OnWorkerThreadCallback callback) {
|
mData = data;
|
mCallback = callback;
|
}
|
|
@WorkerThread
|
@Override
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
mCallback.onServiceConnected(mData,
|
IMultiClientInputMethod.Stub.asInterface(service));
|
}
|
|
@WorkerThread
|
@Override
|
public void onServiceDisconnected(ComponentName name) {
|
mCallback.onServiceDisconnected(mData);
|
}
|
|
@WorkerThread
|
@Override
|
public void onBindingDied(ComponentName name) {
|
mCallback.onBindingDied(mData);
|
}
|
|
Handler getHandler() {
|
return mCallback.getHandler();
|
}
|
}
|
|
PerUserData(@UserIdInt int userId, @Nullable InputMethodInfo inputMethodInfo,
|
@PerUserState int initialState, OnWorkerThreadCallback callback) {
|
mUserId = userId;
|
mCurrentInputMethodInfo = inputMethodInfo;
|
mState = initialState;
|
mOnWorkerThreadServiceConnection =
|
new OnWorkerThreadServiceConnection(this, callback);
|
}
|
|
@GuardedBy("mLock")
|
boolean bindServiceLocked(Context context, @UserIdInt int userId) {
|
final Intent intent =
|
new Intent(MultiClientInputMethodServiceDelegate.SERVICE_INTERFACE)
|
.setComponent(mCurrentInputMethodInfo.getComponent())
|
.putExtra(Intent.EXTRA_CLIENT_LABEL,
|
com.android.internal.R.string.input_method_binding_label)
|
.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
|
context, 0,
|
new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
|
|
// Note: Instead of re-dispatching callback from the main thread to the worker thread
|
// where OnWorkerThreadCallback is running, we pass the Handler object here so that
|
// the callbacks will be directly dispatched to the worker thread.
|
return context.bindServiceAsUser(intent, mOnWorkerThreadServiceConnection,
|
IME_CONNECTION_UNIFIED_BIND_FLAGS,
|
mOnWorkerThreadServiceConnection.getHandler(), UserHandle.of(userId));
|
}
|
|
@GuardedBy("mLock")
|
void unbindServiceLocked(Context context) {
|
context.unbindService(mOnWorkerThreadServiceConnection);
|
}
|
|
@GuardedBy("mLock")
|
@Nullable
|
InputMethodClientInfo getClientLocked(IInputMethodClient client) {
|
return mClientMap.get(client.asBinder());
|
}
|
|
@GuardedBy("mLock")
|
@Nullable
|
InputMethodClientInfo getClientFromIdLocked(int clientId) {
|
return mClientIdToClientMap.get(clientId);
|
}
|
|
@GuardedBy("mLock")
|
@Nullable
|
InputMethodClientInfo removeClientLocked(IInputMethodClient client) {
|
final InputMethodClientInfo info = mClientMap.remove(client.asBinder());
|
if (info != null) {
|
mClientIdToClientMap.remove(info.mClientId);
|
}
|
return info;
|
}
|
|
@GuardedBy("mLock")
|
void addClientLocked(int uid, int pid, IInputMethodClient client,
|
int selfReportedDisplayId) {
|
if (getClientLocked(client) != null) {
|
Slog.wtf(TAG, "The same client is added multiple times");
|
return;
|
}
|
final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
|
try {
|
client.asBinder().linkToDeath(deathRecipient, 0);
|
} catch (RemoteException e) {
|
throw new IllegalStateException(e);
|
}
|
final InputMethodClientInfo clientInfo =
|
new InputMethodClientInfo(client, uid, pid, selfReportedDisplayId);
|
clientInfo.mState = InputMethodClientState.REGISTERED;
|
mClientMap.put(client.asBinder(), clientInfo);
|
mClientIdToClientMap.put(clientInfo.mClientId, clientInfo);
|
switch (mState) {
|
case PerUserState.SERVICE_CONNECTED:
|
try {
|
mCurrentInputMethod.addClient(
|
clientInfo.mClientId, clientInfo.mPid, clientInfo.mUid,
|
clientInfo.mSelfReportedDisplayId);
|
clientInfo.mState = InputMethodClientState.WAITING_FOR_IME_SESSION;
|
} catch (RemoteException e) {
|
// TODO(yukawa): Need logging and expected behavior
|
}
|
break;
|
}
|
}
|
|
@GuardedBy("mLock")
|
void onInputMethodConnectedLocked() {
|
final int numClients = mClientMap.size();
|
for (int i = 0; i < numClients; ++i) {
|
final InputMethodClientInfo clientInfo = mClientMap.valueAt(i);
|
switch (clientInfo.mState) {
|
case InputMethodClientState.REGISTERED:
|
// OK
|
break;
|
default:
|
Slog.e(TAG, "Unexpected state=" + clientInfo.mState);
|
return;
|
}
|
try {
|
mCurrentInputMethod.addClient(
|
clientInfo.mClientId, clientInfo.mUid, clientInfo.mPid,
|
clientInfo.mSelfReportedDisplayId);
|
clientInfo.mState = InputMethodClientState.WAITING_FOR_IME_SESSION;
|
} catch (RemoteException e) {
|
}
|
}
|
}
|
|
@GuardedBy("mLock")
|
void onInputMethodDisconnectedLocked() {
|
final int numClients = mClientMap.size();
|
for (int i = 0; i < numClients; ++i) {
|
final InputMethodClientInfo clientInfo = mClientMap.valueAt(i);
|
switch (clientInfo.mState) {
|
case InputMethodClientState.REGISTERED:
|
// Disconnected before onInputMethodConnectedLocked().
|
break;
|
case InputMethodClientState.WAITING_FOR_IME_SESSION:
|
// Disconnected between addClient() and acceptClient().
|
clientInfo.mState = InputMethodClientState.REGISTERED;
|
break;
|
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
|
clientInfo.mState = InputMethodClientState.REGISTERED;
|
clientInfo.mInputMethodSession = null;
|
clientInfo.mMSInputMethodSession = null;
|
if (clientInfo.mWriteChannel != null) {
|
clientInfo.mWriteChannel.dispose();
|
clientInfo.mWriteChannel = null;
|
}
|
break;
|
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
|
try {
|
clientInfo.mClient.onUnbindMethod(clientInfo.mBindingSequence,
|
UnbindReason.DISCONNECT_IME);
|
} catch (RemoteException e) {
|
}
|
clientInfo.mState = InputMethodClientState.REGISTERED;
|
clientInfo.mInputMethodSession = null;
|
clientInfo.mMSInputMethodSession = null;
|
if (clientInfo.mWriteChannel != null) {
|
clientInfo.mWriteChannel.dispose();
|
clientInfo.mWriteChannel = null;
|
}
|
break;
|
}
|
}
|
}
|
|
@AnyThread
|
void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
|
synchronized (mLock) {
|
ipw.println("mState=" + mState
|
+ ",mCurrentInputMethod=" + mCurrentInputMethod
|
+ ",mCurrentInputMethodInfo=" + mCurrentInputMethodInfo);
|
|
if (mCurrentInputMethod != null) {
|
// indentation will not be kept. So add visual separator here.
|
ipw.println(">>Dump CurrentInputMethod>>");
|
ipw.flush();
|
try {
|
TransferPipe.dumpAsync(mCurrentInputMethod.asBinder(), fd, args);
|
} catch (IOException | RemoteException e) {
|
ipw.println("Failed to dump input method service: " + e);
|
}
|
ipw.println("<<Dump CurrentInputMethod<<");
|
}
|
|
ipw.println("mDisplayIdToImeWindowTokenMap=");
|
for (TokenInfo info : mDisplayIdToImeWindowTokenMap) {
|
ipw.println(" display=" + info.mDisplayId + ",token="
|
+ info.mToken);
|
}
|
ipw.println("mClientMap=");
|
ipw.increaseIndent();
|
for (int i = 0; i < mClientMap.size(); i++) {
|
|
ipw.println("binder=" + mClientMap.keyAt(i));
|
ipw.println(" InputMethodClientInfo=");
|
InputMethodClientInfo info = mClientMap.valueAt(i);
|
if (info != null) {
|
ipw.increaseIndent();
|
info.dumpLocked(fd, ipw, args);
|
ipw.decreaseIndent();
|
}
|
}
|
ipw.decreaseIndent();
|
ipw.println("mClientIdToClientMap=");
|
ipw.increaseIndent();
|
for (int i = 0; i < mClientIdToClientMap.size(); i++) {
|
ipw.println("clientId=" + mClientIdToClientMap.keyAt(i));
|
ipw.println(" InputMethodClientInfo=");
|
InputMethodClientInfo info = mClientIdToClientMap.valueAt(i);
|
if (info != null) {
|
ipw.increaseIndent();
|
info.dumpLocked(fd, ipw, args);
|
ipw.decreaseIndent();
|
}
|
if (info.mClient != null) {
|
// indentation will not be kept. So add visual separator here.
|
ipw.println(">>DumpClientStart>>");
|
ipw.flush(); // all writes should be flushed to guarantee order.
|
try {
|
TransferPipe.dumpAsync(info.mClient.asBinder(), fd, args);
|
} catch (IOException | RemoteException e) {
|
ipw.println(" Failed to dump client:" + e);
|
}
|
ipw.println("<<DumpClientEnd<<");
|
}
|
}
|
ipw.decreaseIndent();
|
}
|
}
|
|
private static final class ClientDeathRecipient implements IBinder.DeathRecipient {
|
private final PerUserData mPerUserData;
|
private final IInputMethodClient mClient;
|
|
ClientDeathRecipient(PerUserData perUserData, IInputMethodClient client) {
|
mPerUserData = perUserData;
|
mClient = client;
|
}
|
|
@BinderThread
|
@Override
|
public void binderDied() {
|
synchronized (mPerUserData.mLock) {
|
mClient.asBinder().unlinkToDeath(this, 0);
|
|
final InputMethodClientInfo clientInfo =
|
mPerUserData.removeClientLocked(mClient);
|
if (clientInfo == null) {
|
return;
|
}
|
|
if (clientInfo.mWriteChannel != null) {
|
clientInfo.mWriteChannel.dispose();
|
clientInfo.mWriteChannel = null;
|
}
|
if (clientInfo.mInputMethodSession != null) {
|
try {
|
clientInfo.mInputMethodSession.finishSession();
|
} catch (RemoteException e) {
|
}
|
clientInfo.mInputMethodSession = null;
|
}
|
clientInfo.mMSInputMethodSession = null;
|
clientInfo.mState = InputMethodClientState.UNREGISTERED;
|
switch (mPerUserData.mState) {
|
case PerUserState.SERVICE_CONNECTED:
|
try {
|
mPerUserData.mCurrentInputMethod.removeClient(clientInfo.mClientId);
|
} catch (RemoteException e) {
|
// TODO(yukawa): Need logging and expected behavior
|
}
|
break;
|
}
|
}
|
}
|
}
|
}
|
|
/**
|
* Queries for multi-client IME specified with {@code componentName}.
|
*
|
* @param context {@link Context} to be used to query component.
|
* @param userId User ID for which the multi-client IME is queried.
|
* @param componentName {@link ComponentName} to be queried.
|
* @return {@link InputMethodInfo} when multi-client IME is found. Otherwise {@code null}.
|
*/
|
@Nullable
|
private static InputMethodInfo queryInputMethod(Context context, @UserIdInt int userId,
|
@Nullable ComponentName componentName) {
|
if (componentName == null) {
|
return null;
|
}
|
|
// Use for queryIntentServicesAsUser
|
final PackageManager pm = context.getPackageManager();
|
final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
|
new Intent(MultiClientInputMethodServiceDelegate.SERVICE_INTERFACE)
|
.setComponent(componentName),
|
PackageManager.GET_META_DATA, userId);
|
|
if (services.isEmpty()) {
|
Slog.e(TAG, "No IME found");
|
return null;
|
}
|
|
if (services.size() > 1) {
|
Slog.e(TAG, "Only one IME service is supported.");
|
return null;
|
}
|
|
final ResolveInfo ri = services.get(0);
|
ServiceInfo si = ri.serviceInfo;
|
final String imeId = InputMethodInfo.computeId(ri);
|
if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {
|
Slog.e(TAG, imeId + " must have required"
|
+ android.Manifest.permission.BIND_INPUT_METHOD);
|
return null;
|
}
|
|
if (!Build.IS_DEBUGGABLE && (si.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
|
Slog.e(TAG, imeId + " must be pre-installed when Build.IS_DEBUGGABLE is false");
|
return null;
|
}
|
|
try {
|
return new InputMethodInfo(context, ri);
|
} catch (Exception e) {
|
Slog.wtf(TAG, "Unable to load input method " + imeId, e);
|
}
|
return null;
|
}
|
|
/**
|
* Manages the mapping rule from user ID to {@link InputMethodInfo}.
|
*/
|
private static final class UserToInputMethodInfoMap {
|
@GuardedBy("mArray")
|
private final SparseArray<InputMethodInfo> mArray = new SparseArray<>();
|
|
@AnyThread
|
void put(@UserIdInt int userId, InputMethodInfo imi) {
|
synchronized (mArray) {
|
mArray.put(userId, imi);
|
}
|
}
|
|
@AnyThread
|
void remove(@UserIdInt int userId) {
|
synchronized (mArray) {
|
mArray.remove(userId);
|
}
|
}
|
|
@AnyThread
|
@Nullable
|
InputMethodInfo get(@UserIdInt int userId) {
|
synchronized (mArray) {
|
return mArray.get(userId);
|
}
|
}
|
|
@AnyThread
|
List<InputMethodInfo> getAsList(@UserIdInt int userId) {
|
final InputMethodInfo info = get(userId);
|
if (info == null) {
|
return Collections.emptyList();
|
}
|
return Collections.singletonList(info);
|
}
|
|
@AnyThread
|
void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
|
synchronized (mArray) {
|
for (int i = 0; i < mArray.size(); i++) {
|
ipw.println("userId=" + mArray.keyAt(i));
|
ipw.println(" InputMethodInfo=" + mArray.valueAt(i));
|
}
|
}
|
}
|
}
|
|
/**
|
* Takes care of IPCs exposed to the multi-client IME.
|
*/
|
private static final class ImeCallbacks
|
extends IMultiClientInputMethodPrivilegedOperations.Stub {
|
private final PerUserData mPerUserData;
|
private final WindowManagerInternal mIWindowManagerInternal;
|
|
ImeCallbacks(PerUserData perUserData) {
|
mPerUserData = perUserData;
|
mIWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
|
}
|
|
@BinderThread
|
@Override
|
public IBinder createInputMethodWindowToken(int displayId) {
|
synchronized (mPerUserData.mLock) {
|
// We assume the number of tokens would not be that large (up to 10 or so) hence
|
// linear search should be acceptable.
|
final int numTokens = mPerUserData.mDisplayIdToImeWindowTokenMap.size();
|
for (int i = 0; i < numTokens; ++i) {
|
final TokenInfo tokenInfo =
|
mPerUserData.mDisplayIdToImeWindowTokenMap.valueAt(i);
|
// Currently we issue up to one window token per display.
|
if (tokenInfo.mDisplayId == displayId) {
|
return tokenInfo.mToken;
|
}
|
}
|
|
final Binder token = new Binder();
|
Binder.withCleanCallingIdentity(
|
PooledLambda.obtainRunnable(WindowManagerInternal::addWindowToken,
|
mIWindowManagerInternal, token, TYPE_INPUT_METHOD, displayId));
|
mPerUserData.mDisplayIdToImeWindowTokenMap.add(new TokenInfo(token, displayId));
|
return token;
|
}
|
}
|
|
@BinderThread
|
@Override
|
public void deleteInputMethodWindowToken(IBinder token) {
|
synchronized (mPerUserData.mLock) {
|
// We assume the number of tokens would not be that large (up to 10 or so) hence
|
// linear search should be acceptable.
|
final int numTokens = mPerUserData.mDisplayIdToImeWindowTokenMap.size();
|
for (int i = 0; i < numTokens; ++i) {
|
final TokenInfo tokenInfo =
|
mPerUserData.mDisplayIdToImeWindowTokenMap.valueAt(i);
|
if (tokenInfo.mToken == token) {
|
mPerUserData.mDisplayIdToImeWindowTokenMap.remove(tokenInfo);
|
break;
|
}
|
}
|
}
|
}
|
|
@BinderThread
|
@Override
|
public void acceptClient(int clientId, IInputMethodSession inputMethodSession,
|
IMultiClientInputMethodSession multiSessionInputMethodSession,
|
InputChannel writeChannel) {
|
synchronized (mPerUserData.mLock) {
|
final InputMethodClientInfo clientInfo =
|
mPerUserData.getClientFromIdLocked(clientId);
|
if (clientInfo == null) {
|
Slog.e(TAG, "Unknown clientId=" + clientId);
|
return;
|
}
|
switch (clientInfo.mState) {
|
case InputMethodClientState.WAITING_FOR_IME_SESSION:
|
try {
|
clientInfo.mClient.setActive(true, false);
|
} catch (RemoteException e) {
|
// TODO(yukawa): Remove this client.
|
return;
|
}
|
clientInfo.mState = InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT;
|
clientInfo.mWriteChannel = writeChannel;
|
clientInfo.mInputMethodSession = inputMethodSession;
|
clientInfo.mMSInputMethodSession = multiSessionInputMethodSession;
|
break;
|
default:
|
Slog.e(TAG, "Unexpected state=" + clientInfo.mState);
|
break;
|
}
|
}
|
}
|
|
@BinderThread
|
@Override
|
public void reportImeWindowTarget(int clientId, int targetWindowHandle,
|
IBinder imeWindowToken) {
|
synchronized (mPerUserData.mLock) {
|
final InputMethodClientInfo clientInfo =
|
mPerUserData.getClientFromIdLocked(clientId);
|
if (clientInfo == null) {
|
Slog.e(TAG, "Unknown clientId=" + clientId);
|
return;
|
}
|
for (WindowInfo windowInfo : clientInfo.mWindowMap.values()) {
|
if (windowInfo.mWindowHandle == targetWindowHandle) {
|
final IBinder targetWindowToken = windowInfo.mWindowToken;
|
// TODO(yukawa): Report targetWindowToken and targetWindowToken to WMS.
|
if (DEBUG) {
|
Slog.v(TAG, "reportImeWindowTarget"
|
+ " clientId=" + clientId
|
+ " imeWindowToken=" + imeWindowToken
|
+ " targetWindowToken=" + targetWindowToken);
|
}
|
}
|
}
|
// not found.
|
}
|
}
|
|
@BinderThread
|
@Override
|
public boolean isUidAllowedOnDisplay(int displayId, int uid) {
|
return mIWindowManagerInternal.isUidAllowedOnDisplay(displayId, uid);
|
}
|
|
@BinderThread
|
@Override
|
public void setActive(int clientId, boolean active) {
|
synchronized (mPerUserData.mLock) {
|
final InputMethodClientInfo clientInfo =
|
mPerUserData.getClientFromIdLocked(clientId);
|
if (clientInfo == null) {
|
Slog.e(TAG, "Unknown clientId=" + clientId);
|
return;
|
}
|
try {
|
clientInfo.mClient.setActive(active, false /* fullscreen */);
|
} catch (RemoteException e) {
|
return;
|
}
|
}
|
}
|
}
|
|
/**
|
* Takes care of IPCs exposed to the IME client.
|
*/
|
private static final class ApiCallbacks extends IInputMethodManager.Stub {
|
private final Context mContext;
|
private final UserDataMap mUserDataMap;
|
private final UserToInputMethodInfoMap mInputMethodInfoMap;
|
private final AppOpsManager mAppOpsManager;
|
private final WindowManagerInternal mWindowManagerInternal;
|
|
ApiCallbacks(Context context, UserDataMap userDataMap,
|
UserToInputMethodInfoMap inputMethodInfoMap) {
|
mContext = context;
|
mUserDataMap = userDataMap;
|
mInputMethodInfoMap = inputMethodInfoMap;
|
mAppOpsManager = context.getSystemService(AppOpsManager.class);
|
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
|
}
|
|
@AnyThread
|
private boolean checkFocus(int uid, int pid, int displayId) {
|
return mWindowManagerInternal.isInputMethodClientFocus(uid, pid, displayId);
|
}
|
|
@BinderThread
|
@Override
|
public void addClient(IInputMethodClient client, IInputContext inputContext,
|
int selfReportedDisplayId) {
|
final int callingUid = Binder.getCallingUid();
|
final int callingPid = Binder.getCallingPid();
|
final int userId = UserHandle.getUserId(callingUid);
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.e(TAG, "addClient() from unknown userId=" + userId
|
+ " uid=" + callingUid + " pid=" + callingPid);
|
return;
|
}
|
synchronized (data.mLock) {
|
data.addClientLocked(callingUid, callingPid, client, selfReportedDisplayId);
|
}
|
}
|
|
@BinderThread
|
@Override
|
public List<InputMethodInfo> getInputMethodList(@UserIdInt int userId) {
|
if (UserHandle.getCallingUserId() != userId) {
|
mContext.enforceCallingPermission(INTERACT_ACROSS_USERS_FULL, null);
|
}
|
return mInputMethodInfoMap.getAsList(userId);
|
}
|
|
@BinderThread
|
@Override
|
public List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
|
if (UserHandle.getCallingUserId() != userId) {
|
mContext.enforceCallingPermission(INTERACT_ACROSS_USERS_FULL, null);
|
}
|
return mInputMethodInfoMap.getAsList(userId);
|
}
|
|
@BinderThread
|
@Override
|
public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
|
boolean allowsImplicitlySelectedSubtypes) {
|
reportNotSupported();
|
return Collections.emptyList();
|
}
|
|
@BinderThread
|
@Override
|
public InputMethodSubtype getLastInputMethodSubtype() {
|
reportNotSupported();
|
return null;
|
}
|
|
@BinderThread
|
@Override
|
public boolean showSoftInput(
|
IInputMethodClient client, int flags, ResultReceiver resultReceiver) {
|
final int callingUid = Binder.getCallingUid();
|
final int callingPid = Binder.getCallingPid();
|
final int userId = UserHandle.getUserId(callingUid);
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.e(TAG, "showSoftInput() from unknown userId=" + userId
|
+ " uid=" + callingUid + " pid=" + callingPid);
|
return false;
|
}
|
synchronized (data.mLock) {
|
final InputMethodClientInfo clientInfo = data.getClientLocked(client);
|
if (clientInfo == null) {
|
Slog.e(TAG, "showSoftInput. client not found. ignoring.");
|
return false;
|
}
|
if (clientInfo.mUid != callingUid) {
|
Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
|
+ " actual=" + callingUid);
|
return false;
|
}
|
switch (clientInfo.mState) {
|
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
|
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
|
try {
|
clientInfo.mMSInputMethodSession.showSoftInput(flags, resultReceiver);
|
} catch (RemoteException e) {
|
}
|
break;
|
default:
|
if (DEBUG) {
|
Slog.e(TAG, "Ignoring showSoftInput(). clientState="
|
+ clientInfo.mState);
|
}
|
break;
|
}
|
return true;
|
}
|
}
|
|
@BinderThread
|
@Override
|
public boolean hideSoftInput(
|
IInputMethodClient client, int flags, ResultReceiver resultReceiver) {
|
final int callingUid = Binder.getCallingUid();
|
final int callingPid = Binder.getCallingPid();
|
final int userId = UserHandle.getUserId(callingUid);
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.e(TAG, "hideSoftInput() from unknown userId=" + userId
|
+ " uid=" + callingUid + " pid=" + callingPid);
|
return false;
|
}
|
synchronized (data.mLock) {
|
final InputMethodClientInfo clientInfo = data.getClientLocked(client);
|
if (clientInfo == null) {
|
return false;
|
}
|
if (clientInfo.mUid != callingUid) {
|
Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
|
+ " actual=" + callingUid);
|
return false;
|
}
|
switch (clientInfo.mState) {
|
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
|
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
|
try {
|
clientInfo.mMSInputMethodSession.hideSoftInput(flags, resultReceiver);
|
} catch (RemoteException e) {
|
}
|
break;
|
default:
|
if (DEBUG) {
|
Slog.e(TAG, "Ignoring hideSoftInput(). clientState="
|
+ clientInfo.mState);
|
}
|
break;
|
}
|
return true;
|
}
|
}
|
|
@BinderThread
|
@Override
|
public InputBindResult startInputOrWindowGainedFocus(
|
@StartInputReason int startInputReason,
|
@Nullable IInputMethodClient client,
|
@Nullable IBinder windowToken,
|
@StartInputFlags int startInputFlags,
|
@SoftInputModeFlags int softInputMode,
|
int windowFlags,
|
@Nullable EditorInfo editorInfo,
|
@Nullable IInputContext inputContext,
|
@MissingMethodFlags int missingMethods,
|
int unverifiedTargetSdkVersion) {
|
final int callingUid = Binder.getCallingUid();
|
final int callingPid = Binder.getCallingPid();
|
final int userId = UserHandle.getUserId(callingUid);
|
|
if (client == null) {
|
return InputBindResult.INVALID_CLIENT;
|
}
|
|
final boolean packageNameVerified =
|
editorInfo != null && InputMethodUtils.checkIfPackageBelongsToUid(
|
mAppOpsManager, callingUid, editorInfo.packageName);
|
if (editorInfo != null && !packageNameVerified) {
|
Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
|
+ " uid=" + callingUid + " package=" + editorInfo.packageName);
|
return InputBindResult.INVALID_PACKAGE_NAME;
|
}
|
|
final PerUserData data = mUserDataMap.get(userId);
|
if (data == null) {
|
Slog.e(TAG, "startInputOrWindowGainedFocus() from unknown userId=" + userId
|
+ " uid=" + callingUid + " pid=" + callingPid);
|
return InputBindResult.INVALID_USER;
|
}
|
|
synchronized (data.mLock) {
|
final InputMethodClientInfo clientInfo = data.getClientLocked(client);
|
if (clientInfo == null) {
|
return InputBindResult.INVALID_CLIENT;
|
}
|
if (clientInfo.mUid != callingUid) {
|
Slog.e(TAG, "Expected calling UID=" + clientInfo.mUid
|
+ " actual=" + callingUid);
|
return InputBindResult.INVALID_CLIENT;
|
}
|
|
switch (data.mState) {
|
case PerUserState.USER_LOCKED:
|
case PerUserState.SERVICE_NOT_QUERIED:
|
case PerUserState.SERVICE_RECOGNIZED:
|
case PerUserState.WAITING_SERVICE_CONNECTED:
|
case PerUserState.UNBIND_CALLED:
|
return InputBindResult.IME_NOT_CONNECTED;
|
case PerUserState.SERVICE_CONNECTED:
|
// OK
|
break;
|
default:
|
Slog.wtf(TAG, "Unexpected state=" + data.mState);
|
return InputBindResult.IME_NOT_CONNECTED;
|
}
|
|
WindowInfo windowInfo = null;
|
if (windowToken != null) {
|
windowInfo = clientInfo.mWindowMap.get(windowToken);
|
if (windowInfo == null) {
|
windowInfo = new WindowInfo(windowToken, WindowHandleSource.getNext());
|
clientInfo.mWindowMap.put(windowToken, windowInfo);
|
}
|
}
|
|
if (!checkFocus(clientInfo.mUid, clientInfo.mPid,
|
clientInfo.mSelfReportedDisplayId)) {
|
return InputBindResult.NOT_IME_TARGET_WINDOW;
|
}
|
|
if (editorInfo == null) {
|
// So-called dummy InputConnection scenario. For app compatibility, we still
|
// notify this to the IME.
|
switch (clientInfo.mState) {
|
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
|
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
|
final int windowHandle = windowInfo != null
|
? windowInfo.mWindowHandle
|
: MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
|
try {
|
clientInfo.mMSInputMethodSession.startInputOrWindowGainedFocus(
|
inputContext, missingMethods, editorInfo, startInputFlags,
|
softInputMode, windowHandle);
|
} catch (RemoteException e) {
|
}
|
break;
|
}
|
return InputBindResult.NULL_EDITOR_INFO;
|
}
|
|
switch (clientInfo.mState) {
|
case InputMethodClientState.REGISTERED:
|
case InputMethodClientState.WAITING_FOR_IME_SESSION:
|
clientInfo.mBindingSequence++;
|
if (clientInfo.mBindingSequence < 0) {
|
clientInfo.mBindingSequence = 0;
|
}
|
return new InputBindResult(
|
InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
|
null, null, data.mCurrentInputMethodInfo.getId(),
|
clientInfo.mBindingSequence, null);
|
case InputMethodClientState.READY_TO_SEND_FIRST_BIND_RESULT:
|
case InputMethodClientState.ALREADY_SENT_BIND_RESULT:
|
clientInfo.mBindingSequence++;
|
if (clientInfo.mBindingSequence < 0) {
|
clientInfo.mBindingSequence = 0;
|
}
|
// Successful start input.
|
final int windowHandle = windowInfo != null
|
? windowInfo.mWindowHandle
|
: MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
|
try {
|
clientInfo.mMSInputMethodSession.startInputOrWindowGainedFocus(
|
inputContext, missingMethods, editorInfo, startInputFlags,
|
softInputMode, windowHandle);
|
} catch (RemoteException e) {
|
}
|
clientInfo.mState = InputMethodClientState.ALREADY_SENT_BIND_RESULT;
|
return new InputBindResult(
|
InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
|
clientInfo.mInputMethodSession,
|
clientInfo.mWriteChannel.dup(),
|
data.mCurrentInputMethodInfo.getId(),
|
clientInfo.mBindingSequence, null);
|
case InputMethodClientState.UNREGISTERED:
|
Slog.e(TAG, "The client is already unregistered.");
|
return InputBindResult.INVALID_CLIENT;
|
}
|
}
|
return null;
|
}
|
|
@BinderThread
|
@Override
|
public void showInputMethodPickerFromClient(
|
IInputMethodClient client, int auxiliarySubtypeMode) {
|
reportNotSupported();
|
}
|
|
@BinderThread
|
@Override
|
public void showInputMethodPickerFromSystem(
|
IInputMethodClient client, int auxiliarySubtypeMode, int displayId) {
|
reportNotSupported();
|
}
|
|
@BinderThread
|
@Override
|
public void showInputMethodAndSubtypeEnablerFromClient(
|
IInputMethodClient client, String inputMethodId) {
|
reportNotSupported();
|
}
|
|
@BinderThread
|
@Override
|
public boolean isInputMethodPickerShownForTest() {
|
reportNotSupported();
|
return false;
|
}
|
|
@BinderThread
|
@Override
|
public InputMethodSubtype getCurrentInputMethodSubtype() {
|
reportNotSupported();
|
return null;
|
}
|
|
@BinderThread
|
@Override
|
public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes) {
|
reportNotSupported();
|
}
|
|
@BinderThread
|
@Override
|
public int getInputMethodWindowVisibleHeight() {
|
reportNotSupported();
|
return 0;
|
}
|
|
@BinderThread
|
@Override
|
public void reportActivityView(IInputMethodClient parentClient, int childDisplayId,
|
float[] matrixValues) {
|
reportNotSupported();
|
}
|
|
@BinderThread
|
@Override
|
public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
|
@Nullable FileDescriptor err, String[] args, @Nullable ShellCallback callback,
|
ResultReceiver resultReceiver) {
|
}
|
|
@BinderThread
|
@Override
|
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
|
final String prefixChild = " ";
|
pw.println("Current Multi Client Input Method Manager state:");
|
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
|
ipw.println("mUserDataMap=");
|
if (mUserDataMap != null) {
|
ipw.increaseIndent();
|
mUserDataMap.dump(fd, ipw, args);
|
}
|
}
|
}
|
}
|