Java程序  |  1731行  |  71.07 KB

/*
 * 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);
            }
        }
    }
}