content: /*
   * Copyright (C) 2013 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.app;
  
  import android.accessibilityservice.AccessibilityService.Callbacks;
  import android.accessibilityservice.AccessibilityService.IAccessibilityServiceClientWrapper;
  import android.accessibilityservice.AccessibilityServiceInfo;
  import android.accessibilityservice.IAccessibilityServiceClient;
  import android.accessibilityservice.IAccessibilityServiceConnection;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.graphics.Bitmap;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.graphics.Region;
  import android.hardware.display.DisplayManagerGlobal;
  import android.os.Build;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.ParcelFileDescriptor;
  import android.os.Process;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.util.Log;
  import android.view.Display;
  import android.view.InputEvent;
  import android.view.KeyEvent;
  import android.view.Surface;
  import android.view.WindowAnimationFrameStats;
  import android.view.WindowContentFrameStats;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityInteractionClient;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.view.accessibility.AccessibilityWindowInfo;
  import android.view.accessibility.IAccessibilityInteractionConnection;
  
  import com.android.internal.util.function.pooled.PooledLambda;
  
  import libcore.io.IoUtils;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.TimeoutException;
  
  /**
   * Class for interacting with the device's UI by simulation user actions and
   * introspection of the screen content. It relies on the platform accessibility
   * APIs to introspect the screen and to perform some actions on the remote view
   * tree. It also allows injecting of arbitrary raw input events simulating user
   * interaction with keyboards and touch devices. One can think of a UiAutomation
   * as a special type of {@link android.accessibilityservice.AccessibilityService}
   * which does not provide hooks for the service life cycle and exposes other
   * APIs that are useful for UI test automation.
   * <p>
   * The APIs exposed by this class are low-level to maximize flexibility when
   * developing UI test automation tools and libraries. Generally, a UiAutomation
   * client should be using a higher-level library or implement high-level functions.
   * For example, performing a tap on the screen requires construction and injecting
   * of a touch down and up events which have to be delivered to the system by a
   * call to {@link #injectInputEvent(InputEvent, boolean)}.
   * </p>
   * <p>
   * The APIs exposed by this class operate across applications enabling a client
   * to write tests that cover use cases spanning over multiple applications. For
   * example, going to the settings application to change a setting and then
   * interacting with another application whose behavior depends on that setting.
   * </p>
   */
  public final class UiAutomation {
  
      private static final String LOG_TAG = UiAutomation.class.getSimpleName();
  
      private static final boolean DEBUG = false;
  
      private static final int CONNECTION_ID_UNDEFINED = -1;
  
      private static final long CONNECT_TIMEOUT_MILLIS = 5000;
  
      /** Rotation constant: Unfreeze rotation (rotating the device changes its rotation state). */
      public static final int ROTATION_UNFREEZE = -2;
  
      /** Rotation constant: Freeze rotation to its current state. */
      public static final int ROTATION_FREEZE_CURRENT = -1;
  
      /** Rotation constant: Freeze rotation to 0 degrees (natural orientation) */
      public static final int ROTATION_FREEZE_0 = Surface.ROTATION_0;
  
      /** Rotation constant: Freeze rotation to 90 degrees . */
      public static final int ROTATION_FREEZE_90 = Surface.ROTATION_90;
  
      /** Rotation constant: Freeze rotation to 180 degrees . */
      public static final int ROTATION_FREEZE_180 = Surface.ROTATION_180;
  
      /** Rotation constant: Freeze rotation to 270 degrees . */
      public static final int ROTATION_FREEZE_270 = Surface.ROTATION_270;
  
      /**
       * UiAutomation supresses accessibility services by default. This flag specifies that
       * existing accessibility services should continue to run, and that new ones may start.
       * This flag is set when obtaining the UiAutomation from
       * {@link Instrumentation#getUiAutomation(int)}.
       */
      public static final int FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES = 0x00000001;
  
      private final Object mLock = new Object();
  
      private final ArrayList<AccessibilityEvent> mEventQueue = new ArrayList<AccessibilityEvent>();
  
      private final Handler mLocalCallbackHandler;
  
      private final IUiAutomationConnection mUiAutomationConnection;
  
      private HandlerThread mRemoteCallbackThread;
  
      private IAccessibilityServiceClient mClient;
  
      private int mConnectionId = CONNECTION_ID_UNDEFINED;
  
      private OnAccessibilityEventListener mOnAccessibilityEventListener;
  
      private boolean mWaitingForEventDelivery;
  
      private long mLastEventTimeMillis;
  
      private boolean mIsConnecting;
  
      private boolean mIsDestroyed;
  
      private int mFlags;
  
      /**
       * Listener for observing the {@link AccessibilityEvent} stream.
       */
      public static interface OnAccessibilityEventListener {
  
          /**
           * Callback for receiving an {@link AccessibilityEvent}.
           * <p>
           * <strong>Note:</strong> This method is <strong>NOT</strong> executed
           * on the main test thread. The client is responsible for proper
           * synchronization.
           * </p>
           * <p>
           * <strong>Note:</strong> It is responsibility of the client
           * to recycle the received events to minimize object creation.
           * </p>
           *
           * @param event The received event.
           */
          public void onAccessibilityEvent(AccessibilityEvent event);
      }
  
      /**
       * Listener for filtering accessibility events.
       */
      public static interface AccessibilityEventFilter {
  
          /**
           * Callback for determining whether an event is accepted or
           * it is filtered out.
           *
           * @param event The event to process.
           * @return True if the event is accepted, false to filter it out.
           */
          public boolean accept(AccessibilityEvent event);
      }
  
      /**
       * Creates a new instance that will handle callbacks from the accessibility
       * layer on the thread of the provided looper and perform requests for privileged
       * operations on the provided connection.
       *
       * @param looper The looper on which to execute accessibility callbacks.
       * @param connection The connection for performing privileged operations.
       *
       * @hide
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public UiAutomation(Looper looper, IUiAutomationConnection connection) {
          if (looper == null) {
              throw new IllegalArgumentException("Looper cannot be null!");
          }
          if (connection == null) {
              throw new IllegalArgumentException("Connection cannot be null!");
          }
          mLocalCallbackHandler = new Handler(looper);
          mUiAutomationConnection = connection;
      }
  
      /**
       * Connects this UiAutomation to the accessibility introspection APIs with default flags.
       *
       * @hide
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public void connect() {
          connect(0);
      }
  
      /**
       * Connects this UiAutomation to the accessibility introspection APIs.
       *
       * @param flags Any flags to apply to the automation as it gets connected
       *
       * @hide
       */
      public void connect(int flags) {
          synchronized (mLock) {
              throwIfConnectedLocked();
              if (mIsConnecting) {
                  return;
              }
              mIsConnecting = true;
              mRemoteCallbackThread = new HandlerThread("UiAutomation");
              mRemoteCallbackThread.start();
              mClient = new IAccessibilityServiceClientImpl(mRemoteCallbackThread.getLooper());
          }
  
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.connect(mClient, flags);
              mFlags = flags;
          } catch (RemoteException re) {
              throw new RuntimeException("Error while connecting UiAutomation", re);
          }
  
          synchronized (mLock) {
              final long startTimeMillis = SystemClock.uptimeMillis();
              try {
                  while (true) {
                      if (isConnectedLocked()) {
                          break;
                      }
                      final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                      final long remainingTimeMillis = CONNECT_TIMEOUT_MILLIS - elapsedTimeMillis;
                      if (remainingTimeMillis <= 0) {
                          throw new RuntimeException("Error while connecting UiAutomation");
                      }
                      try {
                          mLock.wait(remainingTimeMillis);
                      } catch (InterruptedException ie) {
                          /* ignore */
                      }
                  }
              } finally {
                  mIsConnecting = false;
              }
          }
      }
  
      /**
       * Get the flags used to connect the service.
       *
       * @return The flags used to connect
       *
       * @hide
       */
      public int getFlags() {
          return mFlags;
      }
  
      /**
       * Disconnects this UiAutomation from the accessibility introspection APIs.
       *
       * @hide
       */
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      public void disconnect() {
          synchronized (mLock) {
              if (mIsConnecting) {
                  throw new IllegalStateException(
                          "Cannot call disconnect() while connecting!");
              }
              throwIfNotConnectedLocked();
              mConnectionId = CONNECTION_ID_UNDEFINED;
          }
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.disconnect();
          } catch (RemoteException re) {
              throw new RuntimeException("Error while disconnecting UiAutomation", re);
          } finally {
              mRemoteCallbackThread.quit();
              mRemoteCallbackThread = null;
          }
      }
  
      /**
       * The id of the {@link IAccessibilityInteractionConnection} for querying
       * the screen content. This is here for legacy purposes since some tools use
       * hidden APIs to introspect the screen.
       *
       * @hide
       */
      public int getConnectionId() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              return mConnectionId;
          }
      }
  
      /**
       * Reports if the object has been destroyed
       *
       * @return {code true} if the object has been destroyed.
       *
       * @hide
       */
      public boolean isDestroyed() {
          return mIsDestroyed;
      }
  
      /**
       * Sets a callback for observing the stream of {@link AccessibilityEvent}s.
       * The callbacks are delivered on the main application thread.
       *
       * @param listener The callback.
       */
      public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
          synchronized (mLock) {
              mOnAccessibilityEventListener = listener;
          }
      }
  
      /**
       * Destroy this UiAutomation. After calling this method, attempting to use the object will
       * result in errors.
       *
       * @hide
       */
      @TestApi
      public void destroy() {
          disconnect();
          mIsDestroyed = true;
      }
  
      /**
       * Adopt the permission identity of the shell UID for all permissions. This allows
       * you to call APIs protected permissions which normal apps cannot hold but are
       * granted to the shell UID. If you already adopted all shell permissions by calling
       * this method or {@link #adoptShellPermissionIdentity(String...)} a subsequent call
       * would be a no-op. Note that your permission state becomes that of the shell UID
       * and it is not a combination of your and the shell UID permissions.
       * <p>
       * <strong>Note:<strong/> Calling this method adopts all shell permissions and overrides
       * any subset of adopted permissions via {@link #adoptShellPermissionIdentity(String...)}.
       *
       * @see #adoptShellPermissionIdentity(String...)
       * @see #dropShellPermissionIdentity()
       */
      public void adoptShellPermissionIdentity() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), null);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error executing adopting shell permission identity!", re);
          }
      }
  
      /**
       * Adopt the permission identity of the shell UID only for the provided permissions.
       * This allows you to call APIs protected permissions which normal apps cannot hold
       * but are granted to the shell UID. If you already adopted the specified shell
       * permissions by calling this method or {@link #adoptShellPermissionIdentity()} a
       * subsequent call would be a no-op. Note that your permission state becomes that of the
       * shell UID and it is not a combination of your and the shell UID permissions.
       * <p>
       * <strong>Note:<strong/> Calling this method adopts only the specified shell permissions
       * and overrides all adopted permissions via {@link #adoptShellPermissionIdentity()}.
       *
       * @param permissions The permissions to adopt or <code>null</code> to adopt all.
       *
       * @see #adoptShellPermissionIdentity()
       * @see #dropShellPermissionIdentity()
       */
      public void adoptShellPermissionIdentity(@Nullable String... permissions) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.adoptShellPermissionIdentity(Process.myUid(), permissions);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error executing adopting shell permission identity!", re);
          }
      }
  
      /**
       * Drop the shell permission identity adopted by a previous call to
       * {@link #adoptShellPermissionIdentity()}. If you did not adopt the shell permission
       * identity this method would be a no-op.
       *
       * @see #adoptShellPermissionIdentity()
       */
      public void dropShellPermissionIdentity() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.dropShellPermissionIdentity();
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error executing dropping shell permission identity!", re);
          }
      }
  
      /**
       * Performs a global action. Such an action can be performed at any moment
       * regardless of the current application or user location in that application.
       * For example going back, going home, opening recents, etc.
       *
       * @param action The action to perform.
       * @return Whether the action was successfully performed.
       *
       * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_BACK
       * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_HOME
       * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_NOTIFICATIONS
       * @see android.accessibilityservice.AccessibilityService#GLOBAL_ACTION_RECENTS
       */
      public final boolean performGlobalAction(int action) {
          final IAccessibilityServiceConnection connection;
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              connection = AccessibilityInteractionClient.getInstance()
                      .getConnection(mConnectionId);
          }
          // Calling out without a lock held.
          if (connection != null) {
              try {
                  return connection.performGlobalAction(action);
              } catch (RemoteException re) {
                  Log.w(LOG_TAG, "Error while calling performGlobalAction", re);
              }
          }
          return false;
      }
  
      /**
       * Find the view that has the specified focus type. The search is performed
       * across all windows.
       * <p>
       * <strong>Note:</strong> In order to access the windows you have to opt-in
       * to retrieve the interactive windows by setting the
       * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
       * Otherwise, the search will be performed only in the active window.
       * </p>
       *
       * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or
       *         {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}.
       * @return The node info of the focused view or null.
       *
       * @see AccessibilityNodeInfo#FOCUS_INPUT
       * @see AccessibilityNodeInfo#FOCUS_ACCESSIBILITY
       */
      public AccessibilityNodeInfo findFocus(int focus) {
          return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId,
                  AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, focus);
      }
  
      /**
       * Gets the an {@link AccessibilityServiceInfo} describing this UiAutomation.
       * This method is useful if one wants to change some of the dynamically
       * configurable properties at runtime.
       *
       * @return The accessibility service info.
       *
       * @see AccessibilityServiceInfo
       */
      public final AccessibilityServiceInfo getServiceInfo() {
          final IAccessibilityServiceConnection connection;
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              connection = AccessibilityInteractionClient.getInstance()
                      .getConnection(mConnectionId);
          }
          // Calling out without a lock held.
          if (connection != null) {
              try {
                  return connection.getServiceInfo();
              } catch (RemoteException re) {
                  Log.w(LOG_TAG, "Error while getting AccessibilityServiceInfo", re);
              }
          }
          return null;
      }
  
      /**
       * Sets the {@link AccessibilityServiceInfo} that describes how this
       * UiAutomation will be handled by the platform accessibility layer.
       *
       * @param info The info.
       *
       * @see AccessibilityServiceInfo
       */
      public final void setServiceInfo(AccessibilityServiceInfo info) {
          final IAccessibilityServiceConnection connection;
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              AccessibilityInteractionClient.getInstance().clearCache();
              connection = AccessibilityInteractionClient.getInstance()
                      .getConnection(mConnectionId);
          }
          // Calling out without a lock held.
          if (connection != null) {
              try {
                  connection.setServiceInfo(info);
              } catch (RemoteException re) {
                  Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re);
              }
          }
      }
  
      /**
       * Gets the windows on the screen. This method returns only the windows
       * that a sighted user can interact with, as opposed to all windows.
       * For example, if there is a modal dialog shown and the user cannot touch
       * anything behind it, then only the modal window will be reported
       * (assuming it is the top one). For convenience the returned windows
       * are ordered in a descending layer order, which is the windows that
       * are higher in the Z-order are reported first.
       * <p>
       * <strong>Note:</strong> In order to access the windows you have to opt-in
       * to retrieve the interactive windows by setting the
       * {@link AccessibilityServiceInfo#FLAG_RETRIEVE_INTERACTIVE_WINDOWS} flag.
       * </p>
       *
       * @return The windows if there are windows such, otherwise an empty list.
       */
      public List<AccessibilityWindowInfo> getWindows() {
          final int connectionId;
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              connectionId = mConnectionId;
          }
          // Calling out without a lock held.
          return AccessibilityInteractionClient.getInstance()
                  .getWindows(connectionId);
      }
  
      /**
       * Gets the root {@link AccessibilityNodeInfo} in the active window.
       *
       * @return The root info.
       */
      public AccessibilityNodeInfo getRootInActiveWindow() {
          final int connectionId;
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              connectionId = mConnectionId;
          }
          // Calling out without a lock held.
          return AccessibilityInteractionClient.getInstance()
                  .getRootInActiveWindow(connectionId);
      }
  
      /**
       * A method for injecting an arbitrary input event.
       * <p>
       * <strong>Note:</strong> It is caller's responsibility to recycle the event.
       * </p>
       * @param event The event to inject.
       * @param sync Whether to inject the event synchronously.
       * @return Whether event injection succeeded.
       */
      public boolean injectInputEvent(InputEvent event, boolean sync) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Injecting: " + event + " sync: " + sync);
              }
              // Calling out without a lock held.
              return mUiAutomationConnection.injectInputEvent(event, sync);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while injecting input event!", re);
          }
          return false;
      }
  
      /**
       * A request for WindowManagerService to wait until all animations have completed and input
       * information has been sent from WindowManager to native InputManager.
       *
       * @hide
       */
      @TestApi
      public void syncInputTransactions() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              // Calling out without a lock held.
              mUiAutomationConnection.syncInputTransactions();
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while syncing input transactions!", re);
          }
      }
  
      /**
       * Sets the device rotation. A client can freeze the rotation in
       * desired state or freeze the rotation to its current state or
       * unfreeze the rotation (rotating the device changes its rotation
       * state).
       *
       * @param rotation The desired rotation.
       * @return Whether the rotation was set successfully.
       *
       * @see #ROTATION_FREEZE_0
       * @see #ROTATION_FREEZE_90
       * @see #ROTATION_FREEZE_180
       * @see #ROTATION_FREEZE_270
       * @see #ROTATION_FREEZE_CURRENT
       * @see #ROTATION_UNFREEZE
       */
      public boolean setRotation(int rotation) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          switch (rotation) {
              case ROTATION_FREEZE_0:
              case ROTATION_FREEZE_90:
              case ROTATION_FREEZE_180:
              case ROTATION_FREEZE_270:
              case ROTATION_UNFREEZE:
              case ROTATION_FREEZE_CURRENT: {
                  try {
                      // Calling out without a lock held.
                      mUiAutomationConnection.setRotation(rotation);
                      return true;
                  } catch (RemoteException re) {
                      Log.e(LOG_TAG, "Error while setting rotation!", re);
                  }
              } return false;
              default: {
                  throw new IllegalArgumentException("Invalid rotation.");
              }
          }
      }
  
      /**
       * Executes a command and waits for a specific accessibility event up to a
       * given wait timeout. To detect a sequence of events one can implement a
       * filter that keeps track of seen events of the expected sequence and
       * returns true after the last event of that sequence is received.
       * <p>
       * <strong>Note:</strong> It is caller's responsibility to recycle the returned event.
       * </p>
       * @param command The command to execute.
       * @param filter Filter that recognizes the expected event.
       * @param timeoutMillis The wait timeout in milliseconds.
       *
       * @throws TimeoutException If the expected event is not received within the timeout.
       */
      public AccessibilityEvent executeAndWaitForEvent(Runnable command,
              AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
          // Acquire the lock and prepare for receiving events.
          synchronized (mLock) {
              throwIfNotConnectedLocked();
              mEventQueue.clear();
              // Prepare to wait for an event.
              mWaitingForEventDelivery = true;
          }
  
          // Note: We have to release the lock since calling out with this lock held
          // can bite. We will correctly filter out events from other interactions,
          // so starting to collect events before running the action is just fine.
  
          // We will ignore events from previous interactions.
          final long executionStartTimeMillis = SystemClock.uptimeMillis();
          // Execute the command *without* the lock being held.
          command.run();
  
          List<AccessibilityEvent> receivedEvents = new ArrayList<>();
  
          // Acquire the lock and wait for the event.
          try {
              // Wait for the event.
              final long startTimeMillis = SystemClock.uptimeMillis();
              while (true) {
                  List<AccessibilityEvent> localEvents = new ArrayList<>();
                  synchronized (mLock) {
                      localEvents.addAll(mEventQueue);
                      mEventQueue.clear();
                  }
                  // Drain the event queue
                  while (!localEvents.isEmpty()) {
                      AccessibilityEvent event = localEvents.remove(0);
                      // Ignore events from previous interactions.
                      if (event.getEventTime() < executionStartTimeMillis) {
                          continue;
                      }
                      if (filter.accept(event)) {
                          return event;
                      }
                      receivedEvents.add(event);
                  }
                  // Check if timed out and if not wait.
                  final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
                  final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
                  if (remainingTimeMillis <= 0) {
                      throw new TimeoutException("Expected event not received within: "
                              + timeoutMillis + " ms among: " + receivedEvents);
                  }
                  synchronized (mLock) {
                      if (mEventQueue.isEmpty()) {
                          try {
                              mLock.wait(remainingTimeMillis);
                          } catch (InterruptedException ie) {
                              /* ignore */
                          }
                      }
                  }
              }
          } finally {
              int size = receivedEvents.size();
              for (int i = 0; i < size; i++) {
                  receivedEvents.get(i).recycle();
              }
  
              synchronized (mLock) {
                  mWaitingForEventDelivery = false;
                  mEventQueue.clear();
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Waits for the accessibility event stream to become idle, which is not to
       * have received an accessibility event within <code>idleTimeoutMillis</code>.
       * The total time spent to wait for an idle accessibility event stream is bounded
       * by the <code>globalTimeoutMillis</code>.
       *
       * @param idleTimeoutMillis The timeout in milliseconds between two events
       *            to consider the device idle.
       * @param globalTimeoutMillis The maximal global timeout in milliseconds in
       *            which to wait for an idle state.
       *
       * @throws TimeoutException If no idle state was detected within
       *            <code>globalTimeoutMillis.</code>
       */
      public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
              throws TimeoutException {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
  
              final long startTimeMillis = SystemClock.uptimeMillis();
              if (mLastEventTimeMillis <= 0) {
                  mLastEventTimeMillis = startTimeMillis;
              }
  
              while (true) {
                  final long currentTimeMillis = SystemClock.uptimeMillis();
                  // Did we get idle state within the global timeout?
                  final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
                  final long remainingGlobalTimeMillis =
                          globalTimeoutMillis - elapsedGlobalTimeMillis;
                  if (remainingGlobalTimeMillis <= 0) {
                      throw new TimeoutException("No idle state with idle timeout: "
                              + idleTimeoutMillis + " within global timeout: "
                              + globalTimeoutMillis);
                  }
                  // Did we get an idle state within the idle timeout?
                  final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
                  final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
                  if (remainingIdleTimeMillis <= 0) {
                      return;
                  }
                  try {
                       mLock.wait(remainingIdleTimeMillis);
                  } catch (InterruptedException ie) {
                       /* ignore */
                  }
              }
          }
      }
  
      /**
       * Takes a screenshot.
       *
       * @return The screenshot bitmap on success, null otherwise.
       */
      public Bitmap takeScreenshot() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          Display display = DisplayManagerGlobal.getInstance()
                  .getRealDisplay(Display.DEFAULT_DISPLAY);
          Point displaySize = new Point();
          display.getRealSize(displaySize);
  
          int rotation = display.getRotation();
  
          // Take the screenshot
          Bitmap screenShot = null;
          try {
              // Calling out without a lock held.
              screenShot = mUiAutomationConnection.takeScreenshot(
                      new Rect(0, 0, displaySize.x, displaySize.y), rotation);
              if (screenShot == null) {
                  return null;
              }
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while taking screnshot!", re);
              return null;
          }
  
          // Optimization
          screenShot.setHasAlpha(false);
  
          return screenShot;
      }
  
      /**
       * Sets whether this UiAutomation to run in a "monkey" mode. Applications can query whether
       * they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing
       * potentially undesirable actions such as calling 911 or posting on public forums etc.
       *
       * @param enable whether to run in a "monkey" mode or not. Default is not.
       * @see ActivityManager#isUserAMonkey()
       */
      public void setRunAsMonkey(boolean enable) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              ActivityManager.getService().setUserIsMonkey(enable);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error while setting run as monkey!", re);
          }
      }
  
      /**
       * Clears the frame statistics for the content of a given window. These
       * statistics contain information about the most recently rendered content
       * frames.
       *
       * @param windowId The window id.
       * @return Whether the window is present and its frame statistics
       *         were cleared.
       *
       * @see android.view.WindowContentFrameStats
       * @see #getWindowContentFrameStats(int)
       * @see #getWindows()
       * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
       */
      public boolean clearWindowContentFrameStats(int windowId) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Clearing content frame stats for window: " + windowId);
              }
              // Calling out without a lock held.
              return mUiAutomationConnection.clearWindowContentFrameStats(windowId);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error clearing window content frame stats!", re);
          }
          return false;
      }
  
      /**
       * Gets the frame statistics for a given window. These statistics contain
       * information about the most recently rendered content frames.
       * <p>
       * A typical usage requires clearing the window frame statistics via {@link
       * #clearWindowContentFrameStats(int)} followed by an interaction with the UI and
       * finally getting the window frame statistics via calling this method.
       * </p>
       * <pre>
       * // Assume we have at least one window.
       * final int windowId = getWindows().get(0).getId();
       *
       * // Start with a clean slate.
       * uiAutimation.clearWindowContentFrameStats(windowId);
       *
       * // Do stuff with the UI.
       *
       * // Get the frame statistics.
       * WindowContentFrameStats stats = uiAutomation.getWindowContentFrameStats(windowId);
       * </pre>
       *
       * @param windowId The window id.
       * @return The window frame statistics, or null if the window is not present.
       *
       * @see android.view.WindowContentFrameStats
       * @see #clearWindowContentFrameStats(int)
       * @see #getWindows()
       * @see AccessibilityWindowInfo#getId() AccessibilityWindowInfo.getId()
       */
      public WindowContentFrameStats getWindowContentFrameStats(int windowId) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Getting content frame stats for window: " + windowId);
              }
              // Calling out without a lock held.
              return mUiAutomationConnection.getWindowContentFrameStats(windowId);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error getting window content frame stats!", re);
          }
          return null;
      }
  
      /**
       * Clears the window animation rendering statistics. These statistics contain
       * information about the most recently rendered window animation frames, i.e.
       * for window transition animations.
       *
       * @see android.view.WindowAnimationFrameStats
       * @see #getWindowAnimationFrameStats()
       * @see android.R.styleable#WindowAnimation
       */
      public void clearWindowAnimationFrameStats() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Clearing window animation frame stats");
              }
              // Calling out without a lock held.
              mUiAutomationConnection.clearWindowAnimationFrameStats();
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error clearing window animation frame stats!", re);
          }
      }
  
      /**
       * Gets the window animation frame statistics. These statistics contain
       * information about the most recently rendered window animation frames, i.e.
       * for window transition animations.
       *
       * <p>
       * A typical usage requires clearing the window animation frame statistics via
       * {@link #clearWindowAnimationFrameStats()} followed by an interaction that causes
       * a window transition which uses a window animation and finally getting the window
       * animation frame statistics by calling this method.
       * </p>
       * <pre>
       * // Start with a clean slate.
       * uiAutimation.clearWindowAnimationFrameStats();
       *
       * // Do stuff to trigger a window transition.
       *
       * // Get the frame statistics.
       * WindowAnimationFrameStats stats = uiAutomation.getWindowAnimationFrameStats();
       * </pre>
       *
       * @return The window animation frame statistics.
       *
       * @see android.view.WindowAnimationFrameStats
       * @see #clearWindowAnimationFrameStats()
       * @see android.R.styleable#WindowAnimation
       */
      public WindowAnimationFrameStats getWindowAnimationFrameStats() {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Getting window animation frame stats");
              }
              // Calling out without a lock held.
              return mUiAutomationConnection.getWindowAnimationFrameStats();
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error getting window animation frame stats!", re);
          }
          return null;
      }
  
      /**
       * Grants a runtime permission to a package.
       * @param packageName The package to which to grant.
       * @param permission The permission to grant.
       * @throws SecurityException if unable to grant the permission.
       */
      public void grantRuntimePermission(String packageName, String permission) {
          grantRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle());
      }
  
      /**
       * @deprecated replaced by
       *             {@link #grantRuntimePermissionAsUser(String, String, UserHandle)}.
       * @hide
       */
      @Deprecated
      @TestApi
      public boolean grantRuntimePermission(String packageName, String permission,
              UserHandle userHandle) {
          grantRuntimePermissionAsUser(packageName, permission, userHandle);
          return true;
      }
  
      /**
       * Grants a runtime permission to a package for a user.
       * @param packageName The package to which to grant.
       * @param permission The permission to grant.
       * @throws SecurityException if unable to grant the permission.
       */
      public void grantRuntimePermissionAsUser(String packageName, String permission,
              UserHandle userHandle) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Granting runtime permission");
              }
              // Calling out without a lock held.
              mUiAutomationConnection.grantRuntimePermission(packageName,
                      permission, userHandle.getIdentifier());
          } catch (Exception e) {
              throw new SecurityException("Error granting runtime permission", e);
          }
      }
  
      /**
       * Revokes a runtime permission from a package.
       * @param packageName The package to which to grant.
       * @param permission The permission to grant.
       * @throws SecurityException if unable to revoke the permission.
       */
      public void revokeRuntimePermission(String packageName, String permission) {
          revokeRuntimePermissionAsUser(packageName, permission, android.os.Process.myUserHandle());
      }
  
      /**
       * @deprecated replaced by
       *             {@link #revokeRuntimePermissionAsUser(String, String, UserHandle)}.
       * @hide
       */
      @Deprecated
      @TestApi
      public boolean revokeRuntimePermission(String packageName, String permission,
              UserHandle userHandle) {
          revokeRuntimePermissionAsUser(packageName, permission, userHandle);
          return true;
      }
  
      /**
       * Revokes a runtime permission from a package.
       * @param packageName The package to which to grant.
       * @param permission The permission to grant.
       * @throws SecurityException if unable to revoke the permission.
       */
      public void revokeRuntimePermissionAsUser(String packageName, String permission,
              UserHandle userHandle) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          try {
              if (DEBUG) {
                  Log.i(LOG_TAG, "Revoking runtime permission");
              }
              // Calling out without a lock held.
              mUiAutomationConnection.revokeRuntimePermission(packageName,
                      permission, userHandle.getIdentifier());
          } catch (Exception e) {
              throw new SecurityException("Error granting runtime permission", e);
          }
      }
  
      /**
       * Executes a shell command. This method returns a file descriptor that points
       * to the standard output stream. The command execution is similar to running
       * "adb shell <command>" from a host connected to the device.
       * <p>
       * <strong>Note:</strong> It is your responsibility to close the returned file
       * descriptor once you are done reading.
       * </p>
       *
       * @param command The command to execute.
       * @return A file descriptor to the standard output stream.
       *
       * @see #adoptShellPermissionIdentity()
       */
      public ParcelFileDescriptor executeShellCommand(String command) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          warnIfBetterCommand(command);
  
          ParcelFileDescriptor source = null;
          ParcelFileDescriptor sink = null;
  
          try {
              ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
              source = pipe[0];
              sink = pipe[1];
  
              // Calling out without a lock held.
              mUiAutomationConnection.executeShellCommand(command, sink, null);
          } catch (IOException ioe) {
              Log.e(LOG_TAG, "Error executing shell command!", ioe);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error executing shell command!", re);
          } finally {
              IoUtils.closeQuietly(sink);
          }
  
          return source;
      }
  
      /**
       * Executes a shell command. This method returns two file descriptors,
       * one that points to the standard output stream (element at index 0), and one that points
       * to the standard input stream (element at index 1). The command execution is similar
       * to running "adb shell <command>" from a host connected to the device.
       * <p>
       * <strong>Note:</strong> It is your responsibility to close the returned file
       * descriptors once you are done reading/writing.
       * </p>
       *
       * @param command The command to execute.
       * @return File descriptors (out, in) to the standard output/input streams.
       *
       * @hide
       */
      @TestApi
      public ParcelFileDescriptor[] executeShellCommandRw(String command) {
          synchronized (mLock) {
              throwIfNotConnectedLocked();
          }
          warnIfBetterCommand(command);
  
          ParcelFileDescriptor source_read = null;
          ParcelFileDescriptor sink_read = null;
  
          ParcelFileDescriptor source_write = null;
          ParcelFileDescriptor sink_write = null;
  
          try {
              ParcelFileDescriptor[] pipe_read = ParcelFileDescriptor.createPipe();
              source_read = pipe_read[0];
              sink_read = pipe_read[1];
  
              ParcelFileDescriptor[] pipe_write = ParcelFileDescriptor.createPipe();
              source_write = pipe_write[0];
              sink_write = pipe_write[1];
  
              // Calling out without a lock held.
              mUiAutomationConnection.executeShellCommand(command, sink_read, source_write);
          } catch (IOException ioe) {
              Log.e(LOG_TAG, "Error executing shell command!", ioe);
          } catch (RemoteException re) {
              Log.e(LOG_TAG, "Error executing shell command!", re);
          } finally {
              IoUtils.closeQuietly(sink_read);
              IoUtils.closeQuietly(source_write);
          }
  
          ParcelFileDescriptor[] result = new ParcelFileDescriptor[2];
          result[0] = source_read;
          result[1] = sink_write;
          return result;
      }
  
      private boolean isConnectedLocked() {
          return mConnectionId != CONNECTION_ID_UNDEFINED;
      }
  
      private void throwIfConnectedLocked() {
          if (mConnectionId != CONNECTION_ID_UNDEFINED) {
              throw new IllegalStateException("UiAutomation not connected!");
          }
      }
  
      private void throwIfNotConnectedLocked() {
          if (!isConnectedLocked()) {
              throw new IllegalStateException("UiAutomation not connected!");
          }
      }
  
      private void warnIfBetterCommand(String cmd) {
          if (cmd.startsWith("pm grant ")) {
              Log.w(LOG_TAG, "UiAutomation.grantRuntimePermission() "
                      + "is more robust and should be used instead of 'pm grant'");
          } else if (cmd.startsWith("pm revoke ")) {
              Log.w(LOG_TAG, "UiAutomation.revokeRuntimePermission() "
                      + "is more robust and should be used instead of 'pm revoke'");
          }
      }
  
      private class IAccessibilityServiceClientImpl extends IAccessibilityServiceClientWrapper {
  
          public IAccessibilityServiceClientImpl(Looper looper) {
              super(null, looper, new Callbacks() {
                  @Override
                  public void init(int connectionId, IBinder windowToken) {
                      synchronized (mLock) {
                          mConnectionId = connectionId;
                          mLock.notifyAll();
                      }
                  }
  
                  @Override
                  public void onServiceConnected() {
                      /* do nothing */
                  }
  
                  @Override
                  public void onInterrupt() {
                      /* do nothing */
                  }
  
                  @Override
                  public boolean onGesture(int gestureId) {
                      /* do nothing */
                      return false;
                  }
  
                  @Override
                  public void onAccessibilityEvent(AccessibilityEvent event) {
                      final OnAccessibilityEventListener listener;
                      synchronized (mLock) {
                          mLastEventTimeMillis = event.getEventTime();
                          if (mWaitingForEventDelivery) {
                              mEventQueue.add(AccessibilityEvent.obtain(event));
                          }
                          mLock.notifyAll();
                          listener = mOnAccessibilityEventListener;
                      }
                      if (listener != null) {
                          // Calling out only without a lock held.
                          mLocalCallbackHandler.sendMessage(PooledLambda.obtainMessage(
                                  OnAccessibilityEventListener::onAccessibilityEvent,
                                  listener, AccessibilityEvent.obtain(event)));
                      }
                  }
  
                  @Override
                  public boolean onKeyEvent(KeyEvent event) {
                      return false;
                  }
  
                  @Override
                  public void onMagnificationChanged(int displayId, @NonNull Region region,
                          float scale, float centerX, float centerY) {
                      /* do nothing */
                  }
  
                  @Override
                  public void onSoftKeyboardShowModeChanged(int showMode) {
                      /* do nothing */
                  }
  
                  @Override
                  public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
                      /* do nothing */
                  }
  
                  @Override
                  public void onFingerprintCapturingGesturesChanged(boolean active) {
                      /* do nothing */
                  }
  
                  @Override
                  public void onFingerprintGesture(int gesture) {
                      /* do nothing */
                  }
  
                  @Override
                  public void onAccessibilityButtonClicked() {
                      /* do nothing */
                  }
  
                  @Override
                  public void onAccessibilityButtonAvailabilityChanged(boolean available) {
                      /* do nothing */
                  }
              });
          }
      }
  }
  