content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.accessibility;
  
  import android.os.Binder;
  import android.os.RemoteException;
  import android.util.Slog;
  import android.view.MagnificationSpec;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
  import android.view.accessibility.IAccessibilityInteractionConnection;
  import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
  import com.android.internal.annotations.GuardedBy;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.atomic.AtomicInteger;
  
  /**
   * If we are stripping and/or replacing the actions from a window, we need to intercept the
   * nodes heading back to the service and swap out the actions.
   */
  public class ActionReplacingCallback extends IAccessibilityInteractionConnectionCallback.Stub {
      private static final boolean DEBUG = false;
      private static final String LOG_TAG = "ActionReplacingCallback";
  
      private final IAccessibilityInteractionConnectionCallback mServiceCallback;
      private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
      private final int mInteractionId;
      private final Object mLock = new Object();
  
      @GuardedBy("mLock")
      List<AccessibilityNodeInfo> mNodesWithReplacementActions;
  
      @GuardedBy("mLock")
      List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
  
      @GuardedBy("mLock")
      AccessibilityNodeInfo mNodeFromOriginalWindow;
  
      // Keep track of whether or not we've been called back for a single node
      @GuardedBy("mLock")
      boolean mSingleNodeCallbackHappened;
  
      // Keep track of whether or not we've been called back for multiple node
      @GuardedBy("mLock")
      boolean mMultiNodeCallbackHappened;
  
      // We shouldn't get any more callbacks after we've called back the original service, but
      // keep track to make sure we catch such strange things
      @GuardedBy("mLock")
      boolean mDone;
  
      public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
              IAccessibilityInteractionConnection connectionWithReplacementActions,
              int interactionId, int interrogatingPid, long interrogatingTid) {
          mServiceCallback = serviceCallback;
          mConnectionWithReplacementActions = connectionWithReplacementActions;
          mInteractionId = interactionId;
  
          // Request the root node of the replacing window
          final long identityToken = Binder.clearCallingIdentity();
          try {
              mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
                      AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
                      interrogatingPid, interrogatingTid, null, null);
          } catch (RemoteException re) {
              if (DEBUG) {
                  Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
              }
              // Pretend we already got a (null) list of replacement nodes
              mMultiNodeCallbackHappened = true;
          } finally {
              Binder.restoreCallingIdentity(identityToken);
          }
      }
  
      @Override
      public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
          boolean readyForCallback;
          synchronized(mLock) {
              if (interactionId == mInteractionId) {
                  mNodeFromOriginalWindow = info;
              } else {
                  Slog.e(LOG_TAG, "Callback with unexpected interactionId");
                  return;
              }
  
              mSingleNodeCallbackHappened = true;
              readyForCallback = mMultiNodeCallbackHappened;
          }
          if (readyForCallback) {
              replaceInfoActionsAndCallService();
          }
      }
  
      @Override
      public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
              int interactionId) {
          boolean callbackForSingleNode;
          boolean callbackForMultipleNodes;
          synchronized(mLock) {
              if (interactionId == mInteractionId) {
                  mNodesFromOriginalWindow = infos;
              } else if (interactionId == mInteractionId + 1) {
                  mNodesWithReplacementActions = infos;
              } else {
                  Slog.e(LOG_TAG, "Callback with unexpected interactionId");
                  return;
              }
              callbackForSingleNode = mSingleNodeCallbackHappened;
              callbackForMultipleNodes = mMultiNodeCallbackHappened;
              mMultiNodeCallbackHappened = true;
          }
          if (callbackForSingleNode) {
              replaceInfoActionsAndCallService();
          }
          if (callbackForMultipleNodes) {
              replaceInfosActionsAndCallService();
          }
      }
  
      @Override
      public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId)
              throws RemoteException {
          // There's no reason to use this class when performing actions. Do something reasonable.
          mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
      }
  
      private void replaceInfoActionsAndCallService() {
          final AccessibilityNodeInfo nodeToReturn;
          synchronized (mLock) {
              if (mDone) {
                  if (DEBUG) {
                      Slog.e(LOG_TAG, "Extra callback");
                  }
                  return;
              }
              if (mNodeFromOriginalWindow != null) {
                  replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
              }
              recycleReplaceActionNodesLocked();
              nodeToReturn = mNodeFromOriginalWindow;
              mDone = true;
          }
          try {
              mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
          } catch (RemoteException re) {
              if (DEBUG) {
                  Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
              }
          }
      }
  
      private void replaceInfosActionsAndCallService() {
          final List<AccessibilityNodeInfo> nodesToReturn;
          synchronized (mLock) {
              if (mDone) {
                  if (DEBUG) {
                      Slog.e(LOG_TAG, "Extra callback");
                  }
                  return;
              }
              if (mNodesFromOriginalWindow != null) {
                  for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
                      replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
                  }
              }
              recycleReplaceActionNodesLocked();
              nodesToReturn = (mNodesFromOriginalWindow == null)
                      ? null : new ArrayList<>(mNodesFromOriginalWindow);
              mDone = true;
          }
          try {
              mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
          } catch (RemoteException re) {
              if (DEBUG) {
                  Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
              }
          }
      }
  
      @GuardedBy("mLock")
      private void replaceActionsOnInfoLocked(AccessibilityNodeInfo info) {
          info.removeAllActions();
          info.setClickable(false);
          info.setFocusable(false);
          info.setContextClickable(false);
          info.setScrollable(false);
          info.setLongClickable(false);
          info.setDismissable(false);
          // We currently only replace actions for the root node
          if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
                  && mNodesWithReplacementActions != null) {
              // This list should always contain a single node with the root ID
              for (int i = 0; i < mNodesWithReplacementActions.size(); i++) {
                  AccessibilityNodeInfo nodeWithReplacementActions =
                          mNodesWithReplacementActions.get(i);
                  if (nodeWithReplacementActions.getSourceNodeId()
                          == AccessibilityNodeInfo.ROOT_NODE_ID) {
                      List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList();
                      if (actions != null) {
                          for (int j = 0; j < actions.size(); j++) {
                              info.addAction(actions.get(j));
                          }
                          // The PIP needs to be able to take accessibility focus
                          info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
                          info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                      }
                      info.setClickable(nodeWithReplacementActions.isClickable());
                      info.setFocusable(nodeWithReplacementActions.isFocusable());
                      info.setContextClickable(nodeWithReplacementActions.isContextClickable());
                      info.setScrollable(nodeWithReplacementActions.isScrollable());
                      info.setLongClickable(nodeWithReplacementActions.isLongClickable());
                      info.setDismissable(nodeWithReplacementActions.isDismissable());
                  }
              }
          }
      }
  
      @GuardedBy("mLock")
      private void recycleReplaceActionNodesLocked() {
          if (mNodesWithReplacementActions == null) return;
          for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) {
              AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i);
              nodeWithReplacementAction.recycle();
          }
          mNodesWithReplacementActions = null;
      }
  }
  