content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.view.accessibility;
  
  import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
  
  import android.Manifest;
  import android.accessibilityservice.AccessibilityServiceInfo;
  import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType;
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.annotation.SdkConstant;
  import android.annotation.SystemApi;
  import android.annotation.SystemService;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.content.pm.ServiceInfo;
  import android.content.res.Resources;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Process;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.util.ArrayMap;
  import android.util.Log;
  import android.util.SparseArray;
  import android.view.IWindow;
  import android.view.View;
  import android.view.accessibility.AccessibilityEvent.EventType;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.IntPair;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
   * and provides facilities for querying the accessibility state of the system.
   * Accessibility events are generated when something notable happens in the user interface,
   * for example an {@link android.app.Activity} starts, the focus or selection of a
   * {@link android.view.View} changes etc. Parties interested in handling accessibility
   * events implement and register an accessibility service which extends
   * {@link android.accessibilityservice.AccessibilityService}.
   *
   * @see AccessibilityEvent
   * @see AccessibilityNodeInfo
   * @see android.accessibilityservice.AccessibilityService
   * @see Context#getSystemService
   * @see Context#ACCESSIBILITY_SERVICE
   */
  @SystemService(Context.ACCESSIBILITY_SERVICE)
  public final class AccessibilityManager {
      private static final boolean DEBUG = false;
  
      private static final String LOG_TAG = "AccessibilityManager";
  
      /** @hide */
      public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001;
  
      /** @hide */
      public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
  
      /** @hide */
      public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004;
  
      /** @hide */
      public static final int DALTONIZER_DISABLED = -1;
  
      /** @hide */
      @UnsupportedAppUsage
      public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0;
  
      /** @hide */
      public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12;
  
      /** @hide */
      public static final int AUTOCLICK_DELAY_DEFAULT = 600;
  
      /**
       * Activity action: Launch UI to manage which accessibility service or feature is assigned
       * to the navigation bar Accessibility button.
       * <p>
       * Input: Nothing.
       * </p>
       * <p>
       * Output: Nothing.
       * </p>
       *
       * @hide
       */
      @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
      public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON =
              "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
  
      /**
       * Annotations for content flag of UI.
       * @hide
       */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(flag = true, prefix = { "FLAG_CONTENT_" }, value = {
              FLAG_CONTENT_ICONS,
              FLAG_CONTENT_TEXT,
              FLAG_CONTENT_CONTROLS
      })
      public @interface ContentFlag {}
  
      /**
       * Use this flag to indicate the content of a UI that times out contains icons.
       *
       * @see #getRecommendedTimeoutMillis(int, int)
       */
      public static final int FLAG_CONTENT_ICONS = 1;
  
      /**
       * Use this flag to indicate the content of a UI that times out contains text.
       *
       * @see #getRecommendedTimeoutMillis(int, int)
       */
      public static final int FLAG_CONTENT_TEXT = 2;
  
      /**
       * Use this flag to indicate the content of a UI that times out contains interactive controls.
       *
       * @see #getRecommendedTimeoutMillis(int, int)
       */
      public static final int FLAG_CONTENT_CONTROLS = 4;
  
      @UnsupportedAppUsage
      static final Object sInstanceSync = new Object();
  
      @UnsupportedAppUsage
      private static AccessibilityManager sInstance;
  
      @UnsupportedAppUsage
      private final Object mLock = new Object();
  
      @UnsupportedAppUsage
      private IAccessibilityManager mService;
  
      @UnsupportedAppUsage
      final int mUserId;
  
      @UnsupportedAppUsage
      final Handler mHandler;
  
      final Handler.Callback mCallback;
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
      boolean mIsEnabled;
  
      int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK;
  
      int mInteractiveUiTimeout;
      int mNonInteractiveUiTimeout;
  
      boolean mIsTouchExplorationEnabled;
  
      @UnsupportedAppUsage(trackingBug = 123768939L)
      boolean mIsHighTextContrastEnabled;
  
      AccessibilityPolicy mAccessibilityPolicy;
  
      @UnsupportedAppUsage
      private final ArrayMap<AccessibilityStateChangeListener, Handler>
              mAccessibilityStateChangeListeners = new ArrayMap<>();
  
      private final ArrayMap<TouchExplorationStateChangeListener, Handler>
              mTouchExplorationStateChangeListeners = new ArrayMap<>();
  
      private final ArrayMap<HighTextContrastChangeListener, Handler>
              mHighTextContrastStateChangeListeners = new ArrayMap<>();
  
      private final ArrayMap<AccessibilityServicesStateChangeListener, Handler>
              mServicesStateChangeListeners = new ArrayMap<>();
  
      /**
       * Map from a view's accessibility id to the list of request preparers set for that view
       */
      private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists;
  
      /**
       * Listener for the system accessibility state. To listen for changes to the
       * accessibility state on the device, implement this interface and register
       * it with the system by calling {@link #addAccessibilityStateChangeListener}.
       */
      public interface AccessibilityStateChangeListener {
  
          /**
           * Called when the accessibility enabled state changes.
           *
           * @param enabled Whether accessibility is enabled.
           */
          void onAccessibilityStateChanged(boolean enabled);
      }
  
      /**
       * Listener for the system touch exploration state. To listen for changes to
       * the touch exploration state on the device, implement this interface and
       * register it with the system by calling
       * {@link #addTouchExplorationStateChangeListener}.
       */
      public interface TouchExplorationStateChangeListener {
  
          /**
           * Called when the touch exploration enabled state changes.
           *
           * @param enabled Whether touch exploration is enabled.
           */
          void onTouchExplorationStateChanged(boolean enabled);
      }
  
      /**
       * Listener for changes to the state of accessibility services. Changes include services being
       * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service.
       * {@see #addAccessibilityServicesStateChangeListener}.
       *
       * @hide
       */
      @TestApi
      public interface AccessibilityServicesStateChangeListener {
  
          /**
           * Called when the state of accessibility services changes.
           *
           * @param manager The manager that is calling back
           */
          void onAccessibilityServicesStateChanged(AccessibilityManager manager);
      }
  
      /**
       * Listener for the system high text contrast state. To listen for changes to
       * the high text contrast state on the device, implement this interface and
       * register it with the system by calling
       * {@link #addHighTextContrastStateChangeListener}.
       *
       * @hide
       */
      public interface HighTextContrastChangeListener {
  
          /**
           * Called when the high text contrast enabled state changes.
           *
           * @param enabled Whether high text contrast is enabled.
           */
          void onHighTextContrastStateChanged(boolean enabled);
      }
  
      /**
       * Policy to inject behavior into the accessibility manager.
       *
       * @hide
       */
      public interface AccessibilityPolicy {
          /**
           * Checks whether accessibility is enabled.
           *
           * @param accessibilityEnabled Whether the accessibility layer is enabled.
           * @return whether accessibility is enabled.
           */
          boolean isEnabled(boolean accessibilityEnabled);
  
          /**
           * Notifies the policy for an accessibility event.
           *
           * @param event The event.
           * @param accessibilityEnabled Whether the accessibility layer is enabled.
           * @param relevantEventTypes The events relevant events.
           * @return The event to dispatch or null.
           */
          @Nullable AccessibilityEvent onAccessibilityEvent(@NonNull AccessibilityEvent event,
                  boolean accessibilityEnabled, @EventType int relevantEventTypes);
  
          /**
           * Gets the list of relevant events.
           *
           * @param relevantEventTypes The relevant events.
           * @return The relevant events to report.
           */
          @EventType int getRelevantEventTypes(@EventType int relevantEventTypes);
  
          /**
           * Gets the list of installed services to report.
           *
           * @param installedService The installed services.
           * @return The services to report.
           */
          @NonNull List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(
                  @Nullable List<AccessibilityServiceInfo> installedService);
  
          /**
           * Gets the list of enabled accessibility services.
           *
           * @param feedbackTypeFlags The feedback type to query for.
           * @param enabledService The enabled services.
           * @return The services to report.
           */
          @Nullable List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
                  @FeedbackType int feedbackTypeFlags,
                  @Nullable List<AccessibilityServiceInfo> enabledService);
      }
  
      private final IAccessibilityManagerClient.Stub mClient =
              new IAccessibilityManagerClient.Stub() {
          @Override
          public void setState(int state) {
              // We do not want to change this immediately as the application may
              // have already checked that accessibility is on and fired an event,
              // that is now propagating up the view tree, Hence, if accessibility
              // is now off an exception will be thrown. We want to have the exception
              // enforcement to guard against apps that fire unnecessary accessibility
              // events when accessibility is off.
              mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget();
          }
  
          @Override
          public void notifyServicesStateChanged(long updatedUiTimeout) {
              updateUiTimeout(updatedUiTimeout);
  
              final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners;
              synchronized (mLock) {
                  if (mServicesStateChangeListeners.isEmpty()) {
                      return;
                  }
                  listeners = new ArrayMap<>(mServicesStateChangeListeners);
              }
  
              int numListeners = listeners.size();
              for (int i = 0; i < numListeners; i++) {
                  final AccessibilityServicesStateChangeListener listener =
                          mServicesStateChangeListeners.keyAt(i);
                  mServicesStateChangeListeners.valueAt(i).post(() -> listener
                          .onAccessibilityServicesStateChanged(AccessibilityManager.this));
              }
          }
  
          @Override
          public void setRelevantEventTypes(int eventTypes) {
              mRelevantEventTypes = eventTypes;
          }
      };
  
      /**
       * Get an AccessibilityManager instance (create one if necessary).
       *
       * @param context Context in which this manager operates.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public static AccessibilityManager getInstance(Context context) {
          synchronized (sInstanceSync) {
              if (sInstance == null) {
                  final int userId;
                  if (Binder.getCallingUid() == Process.SYSTEM_UID
                          || context.checkCallingOrSelfPermission(
                                  Manifest.permission.INTERACT_ACROSS_USERS)
                                          == PackageManager.PERMISSION_GRANTED
                          || context.checkCallingOrSelfPermission(
                                  Manifest.permission.INTERACT_ACROSS_USERS_FULL)
                                          == PackageManager.PERMISSION_GRANTED) {
                      userId = UserHandle.USER_CURRENT;
                  } else {
                      userId = context.getUserId();
                  }
                  sInstance = new AccessibilityManager(context, null, userId);
              }
          }
          return sInstance;
      }
  
      /**
       * Create an instance.
       *
       * @param context A {@link Context}.
       * @param service An interface to the backing service.
       * @param userId User id under which to run.
       *
       * @hide
       */
      public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
          // Constructor can't be chained because we can't create an instance of an inner class
          // before calling another constructor.
          mCallback = new MyCallback();
          mHandler = new Handler(context.getMainLooper(), mCallback);
          mUserId = userId;
          synchronized (mLock) {
              tryConnectToServiceLocked(service);
          }
      }
  
      /**
       * Create an instance.
       *
       * @param handler The handler to use
       * @param service An interface to the backing service.
       * @param userId User id under which to run.
       *
       * @hide
       */
      public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) {
          mCallback = new MyCallback();
          mHandler = handler;
          mUserId = userId;
          synchronized (mLock) {
              tryConnectToServiceLocked(service);
          }
      }
  
      /**
       * @hide
       */
      public IAccessibilityManagerClient getClient() {
          return mClient;
      }
  
      /**
       * @hide
       */
      @VisibleForTesting
      public Handler.Callback getCallback() {
          return mCallback;
      }
  
      /**
       * Returns if the accessibility in the system is enabled.
       *
       * @return True if accessibility is enabled, false otherwise.
       */
      public boolean isEnabled() {
          synchronized (mLock) {
              return mIsEnabled || (mAccessibilityPolicy != null
                      && mAccessibilityPolicy.isEnabled(mIsEnabled));
          }
      }
  
      /**
       * Returns if the touch exploration in the system is enabled.
       *
       * @return True if touch exploration is enabled, false otherwise.
       */
      public boolean isTouchExplorationEnabled() {
          synchronized (mLock) {
              IAccessibilityManager service = getServiceLocked();
              if (service == null) {
                  return false;
              }
              return mIsTouchExplorationEnabled;
          }
      }
  
      /**
       * Returns if the high text contrast in the system is enabled.
       * <p>
       * <strong>Note:</strong> You need to query this only if you application is
       * doing its own rendering and does not rely on the platform rendering pipeline.
       * </p>
       *
       * @return True if high text contrast is enabled, false otherwise.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public boolean isHighTextContrastEnabled() {
          synchronized (mLock) {
              IAccessibilityManager service = getServiceLocked();
              if (service == null) {
                  return false;
              }
              return mIsHighTextContrastEnabled;
          }
      }
  
      /**
       * Sends an {@link AccessibilityEvent}.
       *
       * @param event The event to send.
       *
       * @throws IllegalStateException if accessibility is not enabled.
       *
       * <strong>Note:</strong> The preferred mechanism for sending custom accessibility
       * events is through calling
       * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
       * instead of this method to allow predecessors to augment/filter events sent by
       * their descendants.
       */
      public void sendAccessibilityEvent(AccessibilityEvent event) {
          final IAccessibilityManager service;
          final int userId;
          final AccessibilityEvent dispatchedEvent;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
              event.setEventTime(SystemClock.uptimeMillis());
              if (mAccessibilityPolicy != null) {
                  dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event,
                          mIsEnabled, mRelevantEventTypes);
                  if (dispatchedEvent == null) {
                      return;
                  }
              } else {
                  dispatchedEvent = event;
              }
              if (!isEnabled()) {
                  Looper myLooper = Looper.myLooper();
                  if (myLooper == Looper.getMainLooper()) {
                      throw new IllegalStateException(
                              "Accessibility off. Did you forget to check that?");
                  } else {
                      // If we're not running on the thread with the main looper, it's possible for
                      // the state of accessibility to change between checking isEnabled and
                      // calling this method. So just log the error rather than throwing the
                      // exception.
                      Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled");
                      return;
                  }
              }
              if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) {
                  if (DEBUG) {
                      Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent
                              + " that is not among "
                              + AccessibilityEvent.eventTypeToString(mRelevantEventTypes));
                  }
                  return;
              }
              userId = mUserId;
          }
          try {
              // it is possible that this manager is in the same process as the service but
              // client using it is called through Binder from another process. Example: MMS
              // app adds a SMS notification and the NotificationManagerService calls this method
              long identityToken = Binder.clearCallingIdentity();
              try {
                  service.sendAccessibilityEvent(dispatchedEvent, userId);
              } finally {
                  Binder.restoreCallingIdentity(identityToken);
              }
              if (DEBUG) {
                  Log.i(LOG_TAG, dispatchedEvent + " sent");
              }
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re);
          } finally {
              if (event != dispatchedEvent) {
                  event.recycle();
              }
              dispatchedEvent.recycle();
          }
      }
  
      /**
       * Requests feedback interruption from all accessibility services.
       */
      public void interrupt() {
          final IAccessibilityManager service;
          final int userId;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
              if (!isEnabled()) {
                  Looper myLooper = Looper.myLooper();
                  if (myLooper == Looper.getMainLooper()) {
                      throw new IllegalStateException(
                              "Accessibility off. Did you forget to check that?");
                  } else {
                      // If we're not running on the thread with the main looper, it's possible for
                      // the state of accessibility to change between checking isEnabled and
                      // calling this method. So just log the error rather than throwing the
                      // exception.
                      Log.e(LOG_TAG, "Interrupt called with accessibility disabled");
                      return;
                  }
              }
              userId = mUserId;
          }
          try {
              service.interrupt(userId);
              if (DEBUG) {
                  Log.i(LOG_TAG, "Requested interrupt from all services");
              }
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re);
          }
      }
  
      /**
       * Returns the {@link ServiceInfo}s of the installed accessibility services.
       *
       * @return An unmodifiable list with {@link ServiceInfo}s.
       *
       * @deprecated Use {@link #getInstalledAccessibilityServiceList()}
       */
      @Deprecated
      public List<ServiceInfo> getAccessibilityServiceList() {
          List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList();
          List<ServiceInfo> services = new ArrayList<>();
          final int infoCount = infos.size();
          for (int i = 0; i < infoCount; i++) {
              AccessibilityServiceInfo info = infos.get(i);
              services.add(info.getResolveInfo().serviceInfo);
          }
          return Collections.unmodifiableList(services);
      }
  
      /**
       * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services.
       *
       * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
       */
      public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
          final IAccessibilityManager service;
          final int userId;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return Collections.emptyList();
              }
              userId = mUserId;
          }
  
          List<AccessibilityServiceInfo> services = null;
          try {
              services = service.getInstalledAccessibilityServiceList(userId);
              if (DEBUG) {
                  Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
              }
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
          }
          if (mAccessibilityPolicy != null) {
              services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services);
          }
          if (services != null) {
              return Collections.unmodifiableList(services);
          } else {
              return Collections.emptyList();
          }
      }
  
      /**
       * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services
       * for a given feedback type.
       *
       * @param feedbackTypeFlags The feedback type flags.
       * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
       *
       * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE
       * @see AccessibilityServiceInfo#FEEDBACK_GENERIC
       * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
       * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
       * @see AccessibilityServiceInfo#FEEDBACK_VISUAL
       * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE
       */
      public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
              int feedbackTypeFlags) {
          final IAccessibilityManager service;
          final int userId;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return Collections.emptyList();
              }
              userId = mUserId;
          }
  
          List<AccessibilityServiceInfo> services = null;
          try {
              services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId);
              if (DEBUG) {
                  Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
              }
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
          }
          if (mAccessibilityPolicy != null) {
              services = mAccessibilityPolicy.getEnabledAccessibilityServiceList(
                      feedbackTypeFlags, services);
          }
          if (services != null) {
              return Collections.unmodifiableList(services);
          } else {
              return Collections.emptyList();
          }
      }
  
      /**
       * Registers an {@link AccessibilityStateChangeListener} for changes in
       * the global accessibility state of the system. Equivalent to calling
       * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)}
       * with a null handler.
       *
       * @param listener The listener.
       * @return Always returns {@code true}.
       */
      public boolean addAccessibilityStateChangeListener(
              @NonNull AccessibilityStateChangeListener listener) {
          addAccessibilityStateChangeListener(listener, null);
          return true;
      }
  
      /**
       * Registers an {@link AccessibilityStateChangeListener} for changes in
       * the global accessibility state of the system. If the listener has already been registered,
       * the handler used to call it back is updated.
       *
       * @param listener The listener.
       * @param handler The handler on which the listener should be called back, or {@code null}
       *                for a callback on the process's main handler.
       */
      public void addAccessibilityStateChangeListener(
              @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {
          synchronized (mLock) {
              mAccessibilityStateChangeListeners
                      .put(listener, (handler == null) ? mHandler : handler);
          }
      }
  
      /**
       * Unregisters an {@link AccessibilityStateChangeListener}.
       *
       * @param listener The listener.
       * @return True if the listener was previously registered.
       */
      public boolean removeAccessibilityStateChangeListener(
              @NonNull AccessibilityStateChangeListener listener) {
          synchronized (mLock) {
              int index = mAccessibilityStateChangeListeners.indexOfKey(listener);
              mAccessibilityStateChangeListeners.remove(listener);
              return (index >= 0);
          }
      }
  
      /**
       * Registers a {@link TouchExplorationStateChangeListener} for changes in
       * the global touch exploration state of the system. Equivalent to calling
       * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)}
       * with a null handler.
       *
       * @param listener The listener.
       * @return Always returns {@code true}.
       */
      public boolean addTouchExplorationStateChangeListener(
              @NonNull TouchExplorationStateChangeListener listener) {
          addTouchExplorationStateChangeListener(listener, null);
          return true;
      }
  
      /**
       * Registers an {@link TouchExplorationStateChangeListener} for changes in
       * the global touch exploration state of the system. If the listener has already been
       * registered, the handler used to call it back is updated.
       *
       * @param listener The listener.
       * @param handler The handler on which the listener should be called back, or {@code null}
       *                for a callback on the process's main handler.
       */
      public void addTouchExplorationStateChangeListener(
              @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {
          synchronized (mLock) {
              mTouchExplorationStateChangeListeners
                      .put(listener, (handler == null) ? mHandler : handler);
          }
      }
  
      /**
       * Unregisters a {@link TouchExplorationStateChangeListener}.
       *
       * @param listener The listener.
       * @return True if listener was previously registered.
       */
      public boolean removeTouchExplorationStateChangeListener(
              @NonNull TouchExplorationStateChangeListener listener) {
          synchronized (mLock) {
              int index = mTouchExplorationStateChangeListeners.indexOfKey(listener);
              mTouchExplorationStateChangeListeners.remove(listener);
              return (index >= 0);
          }
      }
  
      /**
       * Registers a {@link AccessibilityServicesStateChangeListener}.
       *
       * @param listener The listener.
       * @param handler The handler on which the listener should be called back, or {@code null}
       *                for a callback on the process's main handler.
       * @hide
       */
      @TestApi
      public void addAccessibilityServicesStateChangeListener(
              @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) {
          synchronized (mLock) {
              mServicesStateChangeListeners
                      .put(listener, (handler == null) ? mHandler : handler);
          }
      }
  
      /**
       * Unregisters a {@link AccessibilityServicesStateChangeListener}.
       *
       * @param listener The listener.
       *
       * @hide
       */
      @TestApi
      public void removeAccessibilityServicesStateChangeListener(
              @NonNull AccessibilityServicesStateChangeListener listener) {
          synchronized (mLock) {
              mServicesStateChangeListeners.remove(listener);
          }
      }
  
      /**
       * Registers a {@link AccessibilityRequestPreparer}.
       */
      public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
          if (mRequestPreparerLists == null) {
              mRequestPreparerLists = new SparseArray<>(1);
          }
          int id = preparer.getAccessibilityViewId();
          List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id);
          if (requestPreparerList == null) {
              requestPreparerList = new ArrayList<>(1);
              mRequestPreparerLists.put(id, requestPreparerList);
          }
          requestPreparerList.add(preparer);
      }
  
      /**
       * Unregisters a {@link AccessibilityRequestPreparer}.
       */
      public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
          if (mRequestPreparerLists == null) {
              return;
          }
          int viewId = preparer.getAccessibilityViewId();
          List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId);
          if (requestPreparerList != null) {
              requestPreparerList.remove(preparer);
              if (requestPreparerList.isEmpty()) {
                  mRequestPreparerLists.remove(viewId);
              }
          }
      }
  
      /**
       * Get the recommended timeout for changes to the UI needed by this user. Controls should remain
       * on the screen for at least this long to give users time to react. Some users may need
       * extra time to review the controls, or to reach them, or to activate assistive technology
       * to activate the controls automatically.
       * <p>
       * Use the combination of content flags to indicate contents of UI. For example, use
       * {@code FLAG_CONTENT_ICONS | FLAG_CONTENT_TEXT} for message notification which contains
       * icons and text, or use {@code FLAG_CONTENT_TEXT | FLAG_CONTENT_CONTROLS} for button dialog
       * which contains text and button controls.
       * <p/>
       *
       * @param originalTimeout The timeout appropriate for users with no accessibility needs.
       * @param uiContentFlags The combination of flags {@link #FLAG_CONTENT_ICONS},
       *                       {@link #FLAG_CONTENT_TEXT} or {@link #FLAG_CONTENT_CONTROLS} to
       *                       indicate the contents of UI.
       * @return The recommended UI timeout for the current user in milliseconds.
       */
      public int getRecommendedTimeoutMillis(int originalTimeout, @ContentFlag int uiContentFlags) {
          boolean hasControls = (uiContentFlags & FLAG_CONTENT_CONTROLS) != 0;
          boolean hasIconsOrText = (uiContentFlags & FLAG_CONTENT_ICONS) != 0
                  || (uiContentFlags & FLAG_CONTENT_TEXT) != 0;
          int recommendedTimeout = originalTimeout;
          if (hasControls) {
              recommendedTimeout = Math.max(recommendedTimeout, mInteractiveUiTimeout);
          }
          if (hasIconsOrText) {
              recommendedTimeout = Math.max(recommendedTimeout, mNonInteractiveUiTimeout);
          }
          return recommendedTimeout;
      }
  
      /**
       * Get the preparers that are registered for an accessibility ID
       *
       * @param id The ID of interest
       * @return The list of preparers, or {@code null} if there are none.
       *
       * @hide
       */
      public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) {
          if (mRequestPreparerLists == null) {
              return null;
          }
          return mRequestPreparerLists.get(id);
      }
  
      /**
       * Registers a {@link HighTextContrastChangeListener} for changes in
       * the global high text contrast state of the system.
       *
       * @param listener The listener.
       *
       * @hide
       */
      public void addHighTextContrastStateChangeListener(
              @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {
          synchronized (mLock) {
              mHighTextContrastStateChangeListeners
                      .put(listener, (handler == null) ? mHandler : handler);
          }
      }
  
      /**
       * Unregisters a {@link HighTextContrastChangeListener}.
       *
       * @param listener The listener.
       *
       * @hide
       */
      public void removeHighTextContrastStateChangeListener(
              @NonNull HighTextContrastChangeListener listener) {
          synchronized (mLock) {
              mHighTextContrastStateChangeListeners.remove(listener);
          }
      }
  
      /**
       * Sets the {@link AccessibilityPolicy} controlling this manager.
       *
       * @param policy The policy.
       *
       * @hide
       */
      public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) {
          synchronized (mLock) {
              mAccessibilityPolicy = policy;
          }
      }
  
      /**
       * Check if the accessibility volume stream is active.
       *
       * @return True if accessibility volume is active (i.e. some service has requested it). False
       * otherwise.
       * @hide
       */
      public boolean isAccessibilityVolumeStreamActive() {
          List<AccessibilityServiceInfo> serviceInfos =
                  getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
          for (int i = 0; i < serviceInfos.size(); i++) {
              if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) {
                  return true;
              }
          }
          return false;
      }
  
      /**
       * Report a fingerprint gesture to accessibility. Only available for the system process.
       *
       * @param keyCode The key code of the gesture
       * @return {@code true} if accessibility consumes the event. {@code false} if not.
       * @hide
       */
      public boolean sendFingerprintGesture(int keyCode) {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return false;
              }
          }
          try {
              return service.sendFingerprintGesture(keyCode);
          } catch (RemoteException e) {
              return false;
          }
      }
  
      /**
       * Returns accessibility window id from window token. Accessibility window id is the one
       * returned from AccessibilityWindowInfo.getId(). Only available for the system process.
       *
       * @param windowToken Window token to find accessibility window id.
       * @return Accessibility window id for the window token.
       *   AccessibilityWindowInfo.UNDEFINED_WINDOW_ID if accessibility window id not available for
       *   the token.
       * @hide
       */
      @SystemApi
      public int getAccessibilityWindowId(@Nullable IBinder windowToken) {
          if (windowToken == null) {
              return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
          }
  
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
              }
          }
          try {
              return service.getAccessibilityWindowId(windowToken);
          } catch (RemoteException e) {
              return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
          }
      }
  
      /**
       * Sets the current state and notifies listeners, if necessary.
       *
       * @param stateFlags The state flags.
       */
      @UnsupportedAppUsage
      private void setStateLocked(int stateFlags) {
          final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
          final boolean touchExplorationEnabled =
                  (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
          final boolean highTextContrastEnabled =
                  (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
  
          final boolean wasEnabled = isEnabled();
          final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
          final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
  
          // Ensure listeners get current state from isZzzEnabled() calls.
          mIsEnabled = enabled;
          mIsTouchExplorationEnabled = touchExplorationEnabled;
          mIsHighTextContrastEnabled = highTextContrastEnabled;
  
          if (wasEnabled != isEnabled()) {
              notifyAccessibilityStateChanged();
          }
  
          if (wasTouchExplorationEnabled != touchExplorationEnabled) {
              notifyTouchExplorationStateChanged();
          }
  
          if (wasHighTextContrastEnabled != highTextContrastEnabled) {
              notifyHighTextContrastStateChanged();
          }
      }
  
      /**
       * Find an installed service with the specified {@link ComponentName}.
       *
       * @param componentName The name to match to the service.
       *
       * @return The info corresponding to the installed service, or {@code null} if no such service
       * is installed.
       * @hide
       */
      public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName(
              ComponentName componentName) {
          final List<AccessibilityServiceInfo> installedServiceInfos =
                  getInstalledAccessibilityServiceList();
          if ((installedServiceInfos == null) || (componentName == null)) {
              return null;
          }
          for (int i = 0; i < installedServiceInfos.size(); i++) {
              if (componentName.equals(installedServiceInfos.get(i).getComponentName())) {
                  return installedServiceInfos.get(i);
              }
          }
          return null;
      }
  
      /**
       * Adds an accessibility interaction connection interface for a given window.
       * @param windowToken The window token to which a connection is added.
       * @param connection The connection.
       *
       * @hide
       */
      public int addAccessibilityInteractionConnection(IWindow windowToken,
              String packageName, IAccessibilityInteractionConnection connection) {
          final IAccessibilityManager service;
          final int userId;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return View.NO_ID;
              }
              userId = mUserId;
          }
          try {
              return service.addAccessibilityInteractionConnection(windowToken, connection,
                      packageName, userId);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
          }
          return View.NO_ID;
      }
  
      /**
       * Removed an accessibility interaction connection interface for a given window.
       * @param windowToken The window token to which a connection is removed.
       *
       * @hide
       */
      public void removeAccessibilityInteractionConnection(IWindow windowToken) {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
          }
          try {
              service.removeAccessibilityInteractionConnection(windowToken);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re);
          }
      }
  
      /**
       * Perform the accessibility shortcut if the caller has permission.
       *
       * @hide
       */
      @SystemApi
      @TestApi
      @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
      public void performAccessibilityShortcut() {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
          }
          try {
              service.performAccessibilityShortcut();
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re);
          }
      }
  
      /**
       * Notifies that the accessibility button in the system's navigation area has been clicked
       *
       * @param displayId The logical display id.
       * @hide
       */
      public void notifyAccessibilityButtonClicked(int displayId) {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
          }
          try {
              service.notifyAccessibilityButtonClicked(displayId);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while dispatching accessibility button click", re);
          }
      }
  
      /**
       * Notifies that the visibility of the accessibility button in the system's navigation area
       * has changed.
       *
       * @param shown {@code true} if the accessibility button is visible within the system
       *                  navigation area, {@code false} otherwise
       * @hide
       */
      public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
          }
          try {
              service.notifyAccessibilityButtonVisibilityChanged(shown);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re);
          }
      }
  
      /**
       * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture
       * window. Intended for use by the System UI only.
       *
       * @param connection The connection to handle the actions. Set to {@code null} to avoid
       * affecting the actions.
       *
       * @hide
       */
      public void setPictureInPictureActionReplacingConnection(
              @Nullable IAccessibilityInteractionConnection connection) {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
              if (service == null) {
                  return;
              }
          }
          try {
              service.setPictureInPictureActionReplacingConnection(connection);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error setting picture in picture action replacement", re);
          }
      }
  
      /**
       * Get the component name of the service currently assigned to the accessibility shortcut.
       *
       * @return The flattened component name
       * @hide
       */
      @TestApi
      @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
      @Nullable
      public String getAccessibilityShortcutService() {
          final IAccessibilityManager service;
          synchronized (mLock) {
              service = getServiceLocked();
          }
          if (service != null) {
              try {
                  return service.getAccessibilityShortcutService();
              } catch (RemoteException re) {
                  re.rethrowFromSystemServer();
              }
          }
          return null;
      }
  
      private IAccessibilityManager getServiceLocked() {
          if (mService == null) {
              tryConnectToServiceLocked(null);
          }
          return mService;
      }
  
      private void tryConnectToServiceLocked(IAccessibilityManager service) {
          if (service == null) {
              IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
              if (iBinder == null) {
                  return;
              }
              service = IAccessibilityManager.Stub.asInterface(iBinder);
          }
  
          try {
              final long userStateAndRelevantEvents = service.addClient(mClient, mUserId);
              setStateLocked(IntPair.first(userStateAndRelevantEvents));
              mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents);
              updateUiTimeout(service.getRecommendedTimeoutMillis());
              mService = service;
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
          }
      }
  
      /**
       * Notifies the registered {@link AccessibilityStateChangeListener}s.
       */
      private void notifyAccessibilityStateChanged() {
          final boolean isEnabled;
          final ArrayMap<AccessibilityStateChangeListener, Handler> listeners;
          synchronized (mLock) {
              if (mAccessibilityStateChangeListeners.isEmpty()) {
                  return;
              }
              isEnabled = isEnabled();
              listeners = new ArrayMap<>(mAccessibilityStateChangeListeners);
          }
  
          final int numListeners = listeners.size();
          for (int i = 0; i < numListeners; i++) {
              final AccessibilityStateChangeListener listener = listeners.keyAt(i);
              listeners.valueAt(i).post(() ->
                      listener.onAccessibilityStateChanged(isEnabled));
          }
      }
  
      /**
       * Notifies the registered {@link TouchExplorationStateChangeListener}s.
       */
      private void notifyTouchExplorationStateChanged() {
          final boolean isTouchExplorationEnabled;
          final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners;
          synchronized (mLock) {
              if (mTouchExplorationStateChangeListeners.isEmpty()) {
                  return;
              }
              isTouchExplorationEnabled = mIsTouchExplorationEnabled;
              listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners);
          }
  
          final int numListeners = listeners.size();
          for (int i = 0; i < numListeners; i++) {
              final TouchExplorationStateChangeListener listener = listeners.keyAt(i);
              listeners.valueAt(i).post(() ->
                      listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
          }
      }
  
      /**
       * Notifies the registered {@link HighTextContrastChangeListener}s.
       */
      private void notifyHighTextContrastStateChanged() {
          final boolean isHighTextContrastEnabled;
          final ArrayMap<HighTextContrastChangeListener, Handler> listeners;
          synchronized (mLock) {
              if (mHighTextContrastStateChangeListeners.isEmpty()) {
                  return;
              }
              isHighTextContrastEnabled = mIsHighTextContrastEnabled;
              listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
          }
  
          final int numListeners = listeners.size();
          for (int i = 0; i < numListeners; i++) {
              final HighTextContrastChangeListener listener = listeners.keyAt(i);
              listeners.valueAt(i).post(() ->
                      listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
          }
      }
  
      /**
       * Update interactive and non-interactive UI timeout.
       *
       * @param uiTimeout A pair of {@code int}s. First integer for interactive one, and second
       *                  integer for non-interactive one.
       */
      private void updateUiTimeout(long uiTimeout) {
          mInteractiveUiTimeout = IntPair.first(uiTimeout);
          mNonInteractiveUiTimeout = IntPair.second(uiTimeout);
      }
  
      /**
       * Determines if the accessibility button within the system navigation area is supported.
       *
       * @return {@code true} if the accessibility button is supported on this device,
       * {@code false} otherwise
       */
      public static boolean isAccessibilityButtonSupported() {
          final Resources res = Resources.getSystem();
          return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
      }
  
      private final class MyCallback implements Handler.Callback {
          public static final int MSG_SET_STATE = 1;
  
          @Override
          public boolean handleMessage(Message message) {
              switch (message.what) {
                  case MSG_SET_STATE: {
                      // See comment at mClient
                      final int state = message.arg1;
                      synchronized (mLock) {
                          setStateLocked(state);
                      }
                  } break;
              }
              return true;
          }
      }
  }
  