Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.accessibility;
     18 
     19 import android.os.Binder;
     20 import android.os.RemoteException;
     21 import android.util.Slog;
     22 import android.view.MagnificationSpec;
     23 import android.view.accessibility.AccessibilityNodeInfo;
     24 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     25 import android.view.accessibility.IAccessibilityInteractionConnection;
     26 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
     27 import com.android.internal.annotations.GuardedBy;
     28 
     29 import java.util.ArrayList;
     30 import java.util.List;
     31 import java.util.concurrent.atomic.AtomicInteger;
     32 
     33 /**
     34  * If we are stripping and/or replacing the actions from a window, we need to intercept the
     35  * nodes heading back to the service and swap out the actions.
     36  */
     37 public class ActionReplacingCallback extends IAccessibilityInteractionConnectionCallback.Stub {
     38     private static final boolean DEBUG = false;
     39     private static final String LOG_TAG = "ActionReplacingCallback";
     40 
     41     private final IAccessibilityInteractionConnectionCallback mServiceCallback;
     42     private final IAccessibilityInteractionConnection mConnectionWithReplacementActions;
     43     private final int mInteractionId;
     44     private final Object mLock = new Object();
     45 
     46     @GuardedBy("mLock")
     47     List<AccessibilityNodeInfo> mNodesWithReplacementActions;
     48 
     49     @GuardedBy("mLock")
     50     List<AccessibilityNodeInfo> mNodesFromOriginalWindow;
     51 
     52     @GuardedBy("mLock")
     53     AccessibilityNodeInfo mNodeFromOriginalWindow;
     54 
     55     // Keep track of whether or not we've been called back for a single node
     56     @GuardedBy("mLock")
     57     boolean mSingleNodeCallbackHappened;
     58 
     59     // Keep track of whether or not we've been called back for multiple node
     60     @GuardedBy("mLock")
     61     boolean mMultiNodeCallbackHappened;
     62 
     63     // We shouldn't get any more callbacks after we've called back the original service, but
     64     // keep track to make sure we catch such strange things
     65     @GuardedBy("mLock")
     66     boolean mDone;
     67 
     68     public ActionReplacingCallback(IAccessibilityInteractionConnectionCallback serviceCallback,
     69             IAccessibilityInteractionConnection connectionWithReplacementActions,
     70             int interactionId, int interrogatingPid, long interrogatingTid) {
     71         mServiceCallback = serviceCallback;
     72         mConnectionWithReplacementActions = connectionWithReplacementActions;
     73         mInteractionId = interactionId;
     74 
     75         // Request the root node of the replacing window
     76         final long identityToken = Binder.clearCallingIdentity();
     77         try {
     78             mConnectionWithReplacementActions.findAccessibilityNodeInfoByAccessibilityId(
     79                     AccessibilityNodeInfo.ROOT_NODE_ID, null, interactionId + 1, this, 0,
     80                     interrogatingPid, interrogatingTid, null, null);
     81         } catch (RemoteException re) {
     82             if (DEBUG) {
     83                 Slog.e(LOG_TAG, "Error calling findAccessibilityNodeInfoByAccessibilityId()");
     84             }
     85             // Pretend we already got a (null) list of replacement nodes
     86             mMultiNodeCallbackHappened = true;
     87         } finally {
     88             Binder.restoreCallingIdentity(identityToken);
     89         }
     90     }
     91 
     92     @Override
     93     public void setFindAccessibilityNodeInfoResult(AccessibilityNodeInfo info, int interactionId) {
     94         boolean readyForCallback;
     95         synchronized(mLock) {
     96             if (interactionId == mInteractionId) {
     97                 mNodeFromOriginalWindow = info;
     98             } else {
     99                 Slog.e(LOG_TAG, "Callback with unexpected interactionId");
    100                 return;
    101             }
    102 
    103             mSingleNodeCallbackHappened = true;
    104             readyForCallback = mMultiNodeCallbackHappened;
    105         }
    106         if (readyForCallback) {
    107             replaceInfoActionsAndCallService();
    108         }
    109     }
    110 
    111     @Override
    112     public void setFindAccessibilityNodeInfosResult(List<AccessibilityNodeInfo> infos,
    113             int interactionId) {
    114         boolean callbackForSingleNode;
    115         boolean callbackForMultipleNodes;
    116         synchronized(mLock) {
    117             if (interactionId == mInteractionId) {
    118                 mNodesFromOriginalWindow = infos;
    119             } else if (interactionId == mInteractionId + 1) {
    120                 mNodesWithReplacementActions = infos;
    121             } else {
    122                 Slog.e(LOG_TAG, "Callback with unexpected interactionId");
    123                 return;
    124             }
    125             callbackForSingleNode = mSingleNodeCallbackHappened;
    126             callbackForMultipleNodes = mMultiNodeCallbackHappened;
    127             mMultiNodeCallbackHappened = true;
    128         }
    129         if (callbackForSingleNode) {
    130             replaceInfoActionsAndCallService();
    131         }
    132         if (callbackForMultipleNodes) {
    133             replaceInfosActionsAndCallService();
    134         }
    135     }
    136 
    137     @Override
    138     public void setPerformAccessibilityActionResult(boolean succeeded, int interactionId)
    139             throws RemoteException {
    140         // There's no reason to use this class when performing actions. Do something reasonable.
    141         mServiceCallback.setPerformAccessibilityActionResult(succeeded, interactionId);
    142     }
    143 
    144     private void replaceInfoActionsAndCallService() {
    145         final AccessibilityNodeInfo nodeToReturn;
    146         synchronized (mLock) {
    147             if (mDone) {
    148                 if (DEBUG) {
    149                     Slog.e(LOG_TAG, "Extra callback");
    150                 }
    151                 return;
    152             }
    153             if (mNodeFromOriginalWindow != null) {
    154                 replaceActionsOnInfoLocked(mNodeFromOriginalWindow);
    155             }
    156             recycleReplaceActionNodesLocked();
    157             nodeToReturn = mNodeFromOriginalWindow;
    158             mDone = true;
    159         }
    160         try {
    161             mServiceCallback.setFindAccessibilityNodeInfoResult(nodeToReturn, mInteractionId);
    162         } catch (RemoteException re) {
    163             if (DEBUG) {
    164                 Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfoResult");
    165             }
    166         }
    167     }
    168 
    169     private void replaceInfosActionsAndCallService() {
    170         final List<AccessibilityNodeInfo> nodesToReturn;
    171         synchronized (mLock) {
    172             if (mDone) {
    173                 if (DEBUG) {
    174                     Slog.e(LOG_TAG, "Extra callback");
    175                 }
    176                 return;
    177             }
    178             if (mNodesFromOriginalWindow != null) {
    179                 for (int i = 0; i < mNodesFromOriginalWindow.size(); i++) {
    180                     replaceActionsOnInfoLocked(mNodesFromOriginalWindow.get(i));
    181                 }
    182             }
    183             recycleReplaceActionNodesLocked();
    184             nodesToReturn = (mNodesFromOriginalWindow == null)
    185                     ? null : new ArrayList<>(mNodesFromOriginalWindow);
    186             mDone = true;
    187         }
    188         try {
    189             mServiceCallback.setFindAccessibilityNodeInfosResult(nodesToReturn, mInteractionId);
    190         } catch (RemoteException re) {
    191             if (DEBUG) {
    192                 Slog.e(LOG_TAG, "Failed to setFindAccessibilityNodeInfosResult");
    193             }
    194         }
    195     }
    196 
    197     @GuardedBy("mLock")
    198     private void replaceActionsOnInfoLocked(AccessibilityNodeInfo info) {
    199         info.removeAllActions();
    200         info.setClickable(false);
    201         info.setFocusable(false);
    202         info.setContextClickable(false);
    203         info.setScrollable(false);
    204         info.setLongClickable(false);
    205         info.setDismissable(false);
    206         // We currently only replace actions for the root node
    207         if ((info.getSourceNodeId() == AccessibilityNodeInfo.ROOT_NODE_ID)
    208                 && mNodesWithReplacementActions != null) {
    209             // This list should always contain a single node with the root ID
    210             for (int i = 0; i < mNodesWithReplacementActions.size(); i++) {
    211                 AccessibilityNodeInfo nodeWithReplacementActions =
    212                         mNodesWithReplacementActions.get(i);
    213                 if (nodeWithReplacementActions.getSourceNodeId()
    214                         == AccessibilityNodeInfo.ROOT_NODE_ID) {
    215                     List<AccessibilityAction> actions = nodeWithReplacementActions.getActionList();
    216                     if (actions != null) {
    217                         for (int j = 0; j < actions.size(); j++) {
    218                             info.addAction(actions.get(j));
    219                         }
    220                         // The PIP needs to be able to take accessibility focus
    221                         info.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
    222                         info.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    223                     }
    224                     info.setClickable(nodeWithReplacementActions.isClickable());
    225                     info.setFocusable(nodeWithReplacementActions.isFocusable());
    226                     info.setContextClickable(nodeWithReplacementActions.isContextClickable());
    227                     info.setScrollable(nodeWithReplacementActions.isScrollable());
    228                     info.setLongClickable(nodeWithReplacementActions.isLongClickable());
    229                     info.setDismissable(nodeWithReplacementActions.isDismissable());
    230                 }
    231             }
    232         }
    233     }
    234 
    235     @GuardedBy("mLock")
    236     private void recycleReplaceActionNodesLocked() {
    237         if (mNodesWithReplacementActions == null) return;
    238         for (int i = mNodesWithReplacementActions.size() - 1; i >= 0; i--) {
    239             AccessibilityNodeInfo nodeWithReplacementAction = mNodesWithReplacementActions.get(i);
    240             nodeWithReplacementAction.recycle();
    241         }
    242         mNodesWithReplacementActions = null;
    243     }
    244 }
    245