/*
* Copyright (C) 2012 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.utils;
import android.content.Context;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.utils.PerformActionUtils;
import java.util.HashSet;
/**
* Utility class for sending commands to ChromeVox.
*/
public class WebInterfaceUtils {
/**
* If injection of accessibility enhancing JavaScript screen-reader is
* enabled.
* <p>
* This property represents a boolean value encoded as an integer (1 is
* true, 0 is false).
*/
private static final String ACCESSIBILITY_SCRIPT_INJECTION = "accessibility_script_injection";
private static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES =
"ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES";
/**
* Direction constant for forward movement within a page.
*/
public static final int DIRECTION_FORWARD = 1;
/**
* Direction constant for backward movement within a page.
*/
public static final int DIRECTION_BACKWARD = -1;
/**
* Action argument to use with
* {@link #performSpecialAction(AccessibilityNodeInfoCompat, int)} to
* instruct ChromeVox to read the currently focused element within the node.
* within the page.
*/
public static final int ACTION_READ_CURRENT_HTML_ELEMENT = -1;
/**
* Action argument to use with
* {@link #performSpecialAction(AccessibilityNodeInfoCompat, int)} to
* instruct ChromeVox to read the title of the page within the node.
*/
public static final int ACTION_READ_PAGE_TITLE_ELEMENT = -2;
/**
* Action argument to use with
* {@link #performSpecialAction(AccessibilityNodeInfoCompat, int)} to
* instruct ChromeVox to stop all speech and automatic actions.
*/
public static final int ACTION_STOP_SPEECH = -3;
/**
* Action argument to use with
* {@link #performSpecialAction(AccessibilityNodeInfoCompat, int, int)} to
* instruct ChromeVox to move into or out of the special content navigation
* mode.
* <p>
* Using this constant also requires specifying a direction.
* {@link #DIRECTION_FORWARD} indicates ChromeVox should move into this
* content navigation mode, {@link #DIRECTION_BACKWARD} indicates ChromeVox
* should move out of this mode.
*/
private static final int ACTION_TOGGLE_SPECIAL_CONTENT = -4;
/**
* Action argument to use with
* {@link #performSpecialAction(AccessibilityNodeInfoCompat, int, int)} to
* instruct ChromeVox to move into or out of the incremental search mode.
* <p>
* Using this constant does not require a direction as it only toggles
* the state.
*/
public static final int ACTION_TOGGLE_INCREMENTAL_SEARCH = -5;
/**
* HTML element argument to use with
* {@link #performNavigationToHtmlElementAction(AccessibilityNodeInfoCompat,
* int, String)} to instruct ChromeVox to move to the next or previous page
* section.
*/
public static final String HTML_ELEMENT_MOVE_BY_SECTION = "SECTION";
/**
* HTML element argument to use with
* {@link #performNavigationToHtmlElementAction(AccessibilityNodeInfoCompat,
* int, String)} to instruct ChromeVox to move to the next or previous link.
*/
public static final String HTML_ELEMENT_MOVE_BY_LINK = "LINK";
/**
* HTML element argument to use with
* {@link #performNavigationToHtmlElementAction(AccessibilityNodeInfoCompat,
* int, String)} to instruct ChromeVox to move to the next or previous list.
*/
public static final String HTML_ELEMENT_MOVE_BY_LIST = "LIST";
/**
* HTML element argument to use with
* {@link #performNavigationToHtmlElementAction(AccessibilityNodeInfoCompat,
* int, String)} to instruct ChromeVox to move to the next or previous
* control.
*/
public static final String HTML_ELEMENT_MOVE_BY_CONTROL = "CONTROL";
/**
* Sends an instruction to ChromeVox to read the specified HTML element in
* the given direction within a node.
* <p>
* WARNING: Calling this method with a source node of
* {@link android.webkit.WebView} has the side effect of closing the IME
* if currently displayed.
*
* @param node The node containing web content with ChromeVox to which the
* message should be sent
* @param direction {@link #DIRECTION_FORWARD} or
* {@link #DIRECTION_BACKWARD}
* @param htmlElement The HTML tag to send
* @return {@code true} if the action was performed, {@code false}
* otherwise.
*/
public static boolean performNavigationToHtmlElementAction(
AccessibilityNodeInfoCompat node, int direction, String htmlElement) {
final int action = (direction == DIRECTION_FORWARD)
? AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT
: AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT;
final Bundle args = new Bundle();
args.putString(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_HTML_ELEMENT_STRING, htmlElement);
return PerformActionUtils.performAction(node, action, args);
}
public static String[] getSupportedHtmlElements(AccessibilityNodeInfoCompat node) {
HashSet<AccessibilityNodeInfoCompat> visitedNodes =
new HashSet<AccessibilityNodeInfoCompat>();
while (node != null) {
if (visitedNodes.contains(node)) {
return null;
}
visitedNodes.add(node);
Bundle bundle = node.getExtras();
CharSequence supportedHtmlElements =
bundle.getCharSequence(ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES);
if (supportedHtmlElements != null) {
return supportedHtmlElements.toString().split(",");
}
node = node.getParent();
}
return null;
}
/**
* Sends an instruction to ChromeVox to navigate by DOM object in
* the given direction within a node.
*
* @param node The node containing web content with ChromeVox to which the
* message should be sent
* @param direction {@link #DIRECTION_FORWARD} or
* {@link #DIRECTION_BACKWARD}
* @return {@code true} if the action was performed, {@code false}
* otherwise.
*/
public static boolean performNavigationByDOMObject(
AccessibilityNodeInfoCompat node, int direction) {
final int action = (direction == DIRECTION_FORWARD)
? AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT
: AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT;
return PerformActionUtils.performAction(node, action);
}
/**
* Sends an instruction to ChromeVox to move within a page at a specified
* granularity in a given direction.
* <p>
* WARNING: Calling this method with a source node of
* {@link android.webkit.WebView} has the side effect of closing the IME
* if currently displayed.
*
* @param node The node containing web content with ChromeVox to which the
* message should be sent
* @param direction {@link #DIRECTION_FORWARD} or
* {@link #DIRECTION_BACKWARD}
* @param granularity The granularity with which to move or a special case argument.
* @return {@code true} if the action was performed, {@code false} otherwise.
*/
public static boolean performNavigationAtGranularityAction(
AccessibilityNodeInfoCompat node, int direction, int granularity) {
final int action = (direction == DIRECTION_FORWARD)
? AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY
: AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY;
final Bundle args = new Bundle();
args.putInt(
AccessibilityNodeInfoCompat.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, granularity);
return PerformActionUtils.performAction(node, action, args);
}
/**
* Sends instruction to ChromeVox to perform one of the special actions
* defined by the ACTION constants in this class.
* <p>
* WARNING: Calling this method with a source node of
* {@link android.webkit.WebView} has the side effect of closing the IME if
* currently displayed.
*
* @param node The node containing web content with ChromeVox to which the
* message should be sent
* @param action The ACTION constant in this class match the special action
* that ChromeVox should perform.
* @return {@code true} if the action was performed, {@code false}
* otherwise.
*/
public static boolean performSpecialAction(AccessibilityNodeInfoCompat node, int action) {
return performSpecialAction(node, action, DIRECTION_FORWARD);
}
/**
* Sends instruction to ChromeVox to perform one of the special actions
* defined by the ACTION constants in this class.
* <p>
* WARNING: Calling this method with a source node of
* {@link android.webkit.WebView} has the side effect of closing the IME if
* currently displayed.
*
* @param node The node containing web content with ChromeVox to which the
* message should be sent
* @param action The ACTION constant in this class match the special action
* that ChromeVox should perform.
* @param direction The DIRECTION constant in this class to add as an extra
* argument to the special action.
* @return {@code true} if the action was performed, {@code false}
* otherwise.
*/
private static boolean performSpecialAction(
AccessibilityNodeInfoCompat node, int action, int direction) {
/*
* We use performNavigationAtGranularity to communicate with ChromeVox
* for these actions because it is side-effect-free. If we use
* performNavigationToHtmlElementAction and ChromeVox isn't injected,
* we'll actually move selection within the fallback implementation. We
* use the granularity field to hold a value that ChromeVox interprets
* as a special command.
*/
return performNavigationAtGranularityAction(node, direction, action /* fake granularity */);
}
/**
* Sends a message to ChromeVox indicating that it should enter or exit
* special content navigation. This is applicable for things like tables and
* math expressions.
* <p>
* NOTE: further navigation should occur at the default movement
* granularity.
*
* @param node The node representing the web content
* @param enabled Whether this mode should be entered or exited
* @return {@code true} if the action was performed, {@code false}
* otherwise.
*/
public static boolean setSpecialContentModeEnabled(
AccessibilityNodeInfoCompat node, boolean enabled) {
final int direction = (enabled) ? DIRECTION_FORWARD : DIRECTION_BACKWARD;
return performSpecialAction(node, ACTION_TOGGLE_SPECIAL_CONTENT, direction);
}
/**
* Determines whether or not the given node contains web content.
*
* @param node The node to evaluate
* @return {@code true} if the node contains web content, {@code false} otherwise
*/
public static boolean supportsWebActions(AccessibilityNodeInfoCompat node) {
return AccessibilityNodeInfoUtils.supportsAnyAction(node,
AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT,
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT);
}
/**
* Determines whether or not the given node contains native web content (and not ChromeVox).
*
* @param node The node to evaluate
* @return {@code true} if the node contains native web content, {@code false} otherwise
*/
public static boolean hasNativeWebContent(AccessibilityNodeInfoCompat node) {
if (node == null) {
return false;
}
if (!supportsWebActions(node)) {
return false;
}
// ChromeVox does not have sub elements, so if the parent element also has web content
// this cannot be ChromeVox.
AccessibilityNodeInfoCompat parent = node.getParent();
if (supportsWebActions(parent)) {
if (parent != null) {
parent.recycle();
}
return true;
}
if (parent != null) {
parent.recycle();
}
// ChromeVox never has child elements
return node.getChildCount() > 0;
}
/**
* Determines whether or not the given node contains ChromeVox content.
*
* @param node The node to evaluate
* @return {@code true} if the node contains ChromeVox content, {@code false} otherwise
*/
public static boolean hasLegacyWebContent(AccessibilityNodeInfoCompat node) {
if (node == null) {
return false;
}
// TODO: Need better checking for native versus legacy web content.
// Right now Firefox is accidentally treated as legacy web content using the current
// detection routines; the `isNodeFromFirefox` check blacklists any Firefox that supports
// the native web actions from being treated as "legacy" content.
// Once we have resolved this issue, remove the `isNodeFromFirefox` disjunct from the check.
if (!supportsWebActions(node) || isNodeFromFirefox(node)) {
return false;
}
// ChromeVox does not have sub elements, so if the parent element also has web content
// this cannot be ChromeVox.
AccessibilityNodeInfoCompat parent = node.getParent();
if (supportsWebActions(parent)) {
if (parent != null) {
parent.recycle();
}
return false;
}
if (parent != null) {
parent.recycle();
}
// ChromeVox never has child elements
return node.getChildCount() == 0;
}
/**
* @return {@code true} if the user has explicitly enabled injection of
* accessibility scripts into web content.
*/
public static boolean isScriptInjectionEnabled(Context context) {
final int injectionSetting = Settings.Secure.getInt(
context.getContentResolver(), ACCESSIBILITY_SCRIPT_INJECTION, 0);
return (injectionSetting == 1);
}
/**
* Returns whether the given node has navigable web content, either legacy (ChromeVox) or native
* web content.
*
* @param context The parent context.
* @param node The node to check for web content.
* @return Whether the given node has navigable web content.
*/
public static boolean hasNavigableWebContent(
Context context, AccessibilityNodeInfoCompat node) {
return (supportsWebActions(node) && isScriptInjectionEnabled(context))
|| hasNativeWebContent(node);
}
/**
* Check if node is web container
*/
public static boolean isWebContainer(AccessibilityNodeInfoCompat node) {
if (node == null) {
return false;
}
return hasNativeWebContent(node) || isNodeFromFirefox(node);
}
private static boolean isNodeFromFirefox(AccessibilityNodeInfoCompat node) {
if (node == null) {
return false;
}
final String packageName = node.getPackageName() != null ?
node.getPackageName().toString() : "";
return packageName.startsWith("org.mozilla.");
}
}