/* * Copyright (C) 2015 Google Inc. * * 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.switchaccess; import android.content.Context; import android.annotation.TargetApi; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.util.SparseArray; import com.android.talkback.R; import com.android.utils.PerformActionUtils; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * Option Scanning node to perform an action on an {@code AccessibilityNodeInfoCompat} */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class AccessibilityNodeActionNode extends OptionScanActionNode { private static final SparseArray<Integer> MAP_FORWARD_GRANULARITY_IDS = new SparseArray<>(); private static final SparseArray<Integer> MAP_BACKWARD_GRANULARITY_IDS = new SparseArray<>(); private final SwitchAccessNodeCompat mNodeCompat; private final AccessibilityNodeInfoCompat.AccessibilityActionCompat mAction; private final Bundle mArgs; /* * If this action can easily be confused with another because, for example, two views * with identical bounds expose the same action, this value is appended to the action's * description so the user can see the duplicated action. -1 indicates no duplication. */ private int mNumberToAppendToDuplicateAction; static { MAP_FORWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER, R.string.switch_access_move_next_character); MAP_FORWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE, R.string.switch_access_move_next_line); MAP_FORWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE, R.string.switch_access_move_next_page); MAP_FORWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH, R.string.switch_access_move_next_paragraph); MAP_FORWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD, R.string.switch_access_move_next_word); MAP_BACKWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_CHARACTER, R.string.switch_access_move_prev_character); MAP_BACKWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_LINE, R.string.switch_access_move_prev_line); MAP_BACKWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PAGE, R.string.switch_access_move_prev_page); MAP_BACKWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_PARAGRAPH, R.string.switch_access_move_prev_paragraph); MAP_BACKWARD_GRANULARITY_IDS.put(AccessibilityNodeInfoCompat.MOVEMENT_GRANULARITY_WORD, R.string.switch_access_move_prev_word); } /** * Note that this object must be recycled to prevent nodeCompat from leaking. * @param nodeCompat The node on which to perform the action * @param action The action the node should perform. */ public AccessibilityNodeActionNode(SwitchAccessNodeCompat nodeCompat, AccessibilityNodeInfoCompat.AccessibilityActionCompat action) { this(nodeCompat, action, null, -1); } /** * Note that this object must be recycled to prevent nodeCompat from leaking. * @param nodeCompat The node on which to perform the action * @param action The action the node should perform. * @param args The arguments that should be used when performing the action */ public AccessibilityNodeActionNode(SwitchAccessNodeCompat nodeCompat, AccessibilityNodeInfoCompat.AccessibilityActionCompat action, Bundle args) { this(nodeCompat, action, args, -1); } /** * Note that this object must be recycled to prevent nodeCompat from leaking. * * @param nodeCompat The node on which to perform the action * @param action The action the node should perform. * @param args The arguments that should be used when performing the action * @param numberToAppendToDuplicateAction If >= 0, the number will be added to the action's * description */ public AccessibilityNodeActionNode(SwitchAccessNodeCompat nodeCompat, AccessibilityNodeInfoCompat.AccessibilityActionCompat action, Bundle args, int numberToAppendToDuplicateAction) { mNodeCompat = nodeCompat.obtainCopy(); mAction = action; mArgs = args; mNumberToAppendToDuplicateAction = numberToAppendToDuplicateAction; } /** * Get the underlying {@code SwitchAccessNodeCompat} * @return The {@code SwitchAccessNodeCompat} on which actions are performed. */ public SwitchAccessNodeCompat getNodeInfoCompat() { return mNodeCompat.obtainCopy(); } /** * Set the number to append to the action's description. Negative values indicate that * no value will be appended. The default number is -1. * * @param numberToAppendToDuplicateAction If 0 or greater, number to be appended to action's * description. */ public void setNumberToAppendToDuplicateAction(int numberToAppendToDuplicateAction) { mNumberToAppendToDuplicateAction = numberToAppendToDuplicateAction; } /** * This method must be called to free the copy of the AccessibilityNodeInfoCompat made * when the object was created */ @Override public void recycle() { mNodeCompat.recycle(); } @Override public boolean equals(Object other) { if (!(other instanceof AccessibilityNodeActionNode)) { return false; } AccessibilityNodeActionNode otherNode = (AccessibilityNodeActionNode) other; if (otherNode.mAction.getId() != mAction.getId()) { return false; } if (!otherNode.mNodeCompat.equals(mNodeCompat)) { return false; } if (!otherNode.getRectsForNodeHighlight().equals( getRectsForNodeHighlight())) { return false; } return true; } @Override public void performAction() { PerformActionUtils.performAction(mNodeCompat, mAction.getId(), mArgs); } @Override public Set<Rect> getRectsForNodeHighlight() { Rect boundsInScreen = new Rect(); mNodeCompat.getVisibleBoundsInScreen(boundsInScreen); Set<Rect> rects = new HashSet<>(); rects.add(boundsInScreen); return Collections.unmodifiableSet(rects); } @Override public CharSequence getActionLabel(Context context) { CharSequence label = getActionLabelInternal(context); if (mNumberToAppendToDuplicateAction >= 0) { String formatString = context.getResources().getString(R.string.switch_access_dup_bounds_format); return String.format( formatString, label.toString(), mNumberToAppendToDuplicateAction); } return label; } public CharSequence getActionLabelInternal(Context context) { CharSequence label = mAction.getLabel(); if (label != null) { return label; } int id = mAction.getId(); switch (id) { case AccessibilityNodeInfoCompat.ACTION_CLICK: return context.getResources().getString(R.string.action_name_click); case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: return context.getResources().getString(R.string.action_name_long_click); case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: return context.getResources().getString(R.string.action_name_scroll_backward); case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: return context.getResources().getString(R.string.action_name_scroll_forward); case AccessibilityNodeInfoCompat.ACTION_DISMISS: return context.getResources().getString(R.string.action_name_dismiss); case AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: { // Movement requires a granularity argument int granularity = mArgs.getInt( AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); Integer stringId = MAP_FORWARD_GRANULARITY_IDS.get(granularity); return (stringId == null) ? null : context.getString(stringId); } case AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { // Movement requires a granularity argument int granularity = mArgs.getInt( AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); Integer stringId = MAP_BACKWARD_GRANULARITY_IDS.get(granularity); return (stringId == null) ? null : context.getString(stringId); } default: return null; } } }