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