diff options
author | Maurice Lam <yukl@google.com> | 2015-09-24 14:25:11 -0700 |
---|---|---|
committer | Maurice Lam <yukl@google.com> | 2015-09-24 16:28:27 -0700 |
commit | 655ddae8b52191633f0052617feb511b0bc02945 (patch) | |
tree | 75353d0262ce4318d3623e5a993d8d56548e3f99 /src | |
parent | 16a8e8d1028c09abe8b083acf9da70e6d65ebb79 (diff) | |
download | packages_apps_Settings-655ddae8b52191633f0052617feb511b0bc02945.zip packages_apps_Settings-655ddae8b52191633f0052617feb511b0bc02945.tar.gz packages_apps_Settings-655ddae8b52191633f0052617feb511b0bc02945.tar.bz2 |
[Fingerprint] Fix link accessibility
Copied ExploreByTouchHelper, LinkAccessibilityHelper and LinkTextView
from setup wizard and switched fingerprint_settings_footer to use
LinkTextView, so that users can navigate to the link in TalkBack
mode.
Bug: 24343933
Change-Id: I0ff0c054510dfef4263ea223cb0044a4bddf321f
Diffstat (limited to 'src')
3 files changed, 942 insertions, 0 deletions
diff --git a/src/com/android/settings/widget/ExploreByTouchHelper.java b/src/com/android/settings/widget/ExploreByTouchHelper.java new file mode 100644 index 0000000..b64a74c --- /dev/null +++ b/src/com/android/settings/widget/ExploreByTouchHelper.java @@ -0,0 +1,724 @@ +/* + * 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 com.android.settings.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.LinkedList; +import java.util.List; + +/** + * Copied from setup wizard, which is in turn a modified copy of + * com.android.internal.ExploreByTouchHelper with the following modifications: + * + * - Make accessibility calls to the views, instead of to the accessibility delegate directly to + * make sure those methods for View subclasses are called. + * + * ExploreByTouchHelper is a utility class for implementing accessibility + * support in custom {@link android.view.View}s that represent a collection of View-like + * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and + * simplifies many aspects of providing information to accessibility services + * and managing accessibility focus. This class does not currently support + * hierarchies of logical items. + * <p> + * This should be applied to the parent view using + * {@link android.view.View#setAccessibilityDelegate}: + * + * <pre> + * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback); + * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper); + * </pre> + */ +public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + /** Default class name used for virtual views. */ + private static final String DEFAULT_CLASS_NAME = View.class.getName(); + + // Temporary, reusable data structures. + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + /** View's context **/ + private Context mContext; + + /** System accessibility manager, used to check state and send events. */ + private final AccessibilityManager mManager; + + /** View whose internal structure is exposed through this helper. */ + private final View mView; + + /** Node provider that handles creating nodes and performing actions. */ + private ExploreByTouchNodeProvider mNodeProvider; + + /** Virtual view id for the currently focused logical item. */ + private int mFocusedVirtualViewId = INVALID_ID; + + /** Virtual view id for the currently hovered logical item. */ + private int mHoveredVirtualViewId = INVALID_ID; + + /** + * Factory method to create a new {@link com.google.android.setupwizard.util.ExploreByTouchHelper}. + * + * @param forView View whose logical children are exposed by this helper. + */ + public ExploreByTouchHelper(View forView) { + if (forView == null) { + throw new IllegalArgumentException("View may not be null"); + } + + mView = forView; + mContext = forView.getContext(); + mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + /** + * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper. + * + * @param host View whose logical children are exposed by this helper. + * @return The accessibility node provider for this helper. + */ + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + if (mNodeProvider == null) { + mNodeProvider = new ExploreByTouchNodeProvider(); + } + return mNodeProvider; + } + + /** + * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when + * the Explore by Touch feature is enabled. + * <p> + * This method should be called by overriding + * {@link android.view.View#dispatchHoverEvent}: + * + * <pre>@Override + * public boolean dispatchHoverEvent(MotionEvent event) { + * if (mHelper.dispatchHoverEvent(this, event) { + * return true; + * } + * return super.dispatchHoverEvent(event); + * } + * </pre> + * + * @param event The hover event to dispatch to the virtual view hierarchy. + * @return Whether the hover event was handled. + */ + public boolean dispatchHoverEvent(MotionEvent event) { + if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_ENTER: + final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); + updateHoveredVirtualView(virtualViewId); + return (virtualViewId != INVALID_ID); + case MotionEvent.ACTION_HOVER_EXIT: + if (mFocusedVirtualViewId != INVALID_ID) { + updateHoveredVirtualView(INVALID_ID); + return true; + } + return false; + default: + return false; + } + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + * <p> + * You should call this method after performing a user action that normally + * fires an accessibility event, such as clicking on an item. + * + * <pre>public void performItemClick(T item) { + * ... + * sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED); + * } + * </pre> + * + * @param virtualViewId The virtual view id for which to send an event. + * @param eventType The type of event to send. + * @return true if the event was sent successfully. + */ + public boolean sendEventForVirtualView(int virtualViewId, int eventType) { + if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { + return false; + } + + final ViewParent parent = mView.getParent(); + if (parent == null) { + return false; + } + + final AccessibilityEvent event = createEvent(virtualViewId, eventType); + return parent.requestSendAccessibilityEvent(mView, event); + } + + /** + * Notifies the accessibility framework that the properties of the parent + * view have changed. + * <p> + * You <b>must</b> call this method after adding or removing items from the + * parent view. + */ + public void invalidateRoot() { + invalidateVirtualView(View.NO_ID); + } + + /** + * Notifies the accessibility framework that the properties of a particular + * item have changed. + * <p> + * You <b>must</b> call this method after changing any of the properties set + * in {@link #onPopulateNodeForVirtualView}. + * + * @param virtualViewId The virtual view id to invalidate. + */ + public void invalidateVirtualView(int virtualViewId) { + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the virtual view id for the currently focused item, + * + * @return A virtual view id, or {@link #INVALID_ID} if no item is + * currently focused. + */ + public int getFocusedVirtualView() { + return mFocusedVirtualViewId; + } + + /** + * Sets the currently hovered item, sending hover accessibility events as + * necessary to maintain the correct state. + * + * @param virtualViewId The virtual view id for the item currently being + * hovered, or {@link #INVALID_ID} if no item is hovered within + * the parent view. + */ + private void updateHoveredVirtualView(int virtualViewId) { + if (mHoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = mHoveredVirtualViewId; + mHoveredVirtualViewId = virtualViewId; + + // Stay consistent with framework behavior by sending ENTER/EXIT pairs + // in reverse order. This is accurate as of API 18. + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} for the specified + * virtual view id, which includes the host view ({@link android.view.View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEvent(int virtualViewId, int eventType) { + switch (virtualViewId) { + case View.NO_ID: + return createEventForHost(eventType); + default: + return createEventForChild(virtualViewId, eventType); + } + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} for the host node. + * + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForHost(int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + mView.onInitializeAccessibilityEvent(event); + return event; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} populated with + * information about the specified item. + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setEnabled(true); + event.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the event. + onPopulateEventForVirtualView(virtualViewId, event); + + // Make sure the developer is following the rules. + if (event.getText().isEmpty() && (event.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateEventForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + event.setPackageName(mView.getContext().getPackageName()); + event.setSource(mView, virtualViewId); + + return event; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * specified virtual view id, which includes the host view + * ({@link android.view.View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct a node. + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information + * about the specified item. + */ + private AccessibilityNodeInfo createNode(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createNodeForHost(); + default: + return createNodeForChild(virtualViewId); + } + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * host view populated with its virtual descendants. + * + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} for the parent node. + */ + private AccessibilityNodeInfo createNodeForHost() { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView); + mView.onInitializeAccessibilityNodeInfo(node); + + // Add the virtual descendants. + final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>(); + getVisibleVirtualViews(virtualViewIds); + + for (Integer childVirtualViewId : virtualViewIds) { + node.addChild(mView, childVirtualViewId); + } + + return node; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * specified item. Automatically manages accessibility focus actions. + * <p> + * Allows the implementing class to specify most node properties, but + * overrides the following: + * <ul> + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setPackageName} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setClassName} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setParent(android.view.View)} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setSource(android.view.View, int)} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen(android.graphics.Rect)} + * </ul> + * <p> + * Uses the bounds of the parent view and the parent-relative bounding + * rectangle specified by + * {@link android.view.accessibility.AccessibilityNodeInfo#getBoundsInParent} to automatically + * update the following properties: + * <ul> + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent} + * </ul> + * + * @param virtualViewId The virtual view id for item for which to construct + * a node. + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} for the specified item. + */ + private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); + + // Ensure the client has good defaults. + node.setEnabled(true); + node.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the node. + onPopulateNodeForVirtualView(virtualViewId, node); + + // Make sure the developer is following the rules. + if ((node.getText() == null) && (node.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateNodeForVirtualViewId()"); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("Callbacks must set parent bounds in " + + "populateNodeForVirtualViewId()"); + } + + final int actions = node.getActions(); + if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mView.getContext().getPackageName()); + node.setSource(mView, virtualViewId); + node.setParent(mView); + + // Manage internal accessibility focus state. + if (mFocusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + private boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: + return performActionForHost(action, arguments); + default: + return performActionForChild(virtualViewId, action, arguments); + } + } + + private boolean performActionForHost(int action, Bundle arguments) { + return mView.performAccessibilityAction(action, arguments); + } + + private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return manageFocusForChild(virtualViewId, action, arguments); + default: + return onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } + + private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + return requestAccessibilityFocus(virtualViewId); + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return clearAccessibilityFocus(virtualViewId); + default: + return false; + } + } + + /** + * Computes whether the specified {@link android.graphics.Rect} intersects with the visible + * portion of its parent {@link android.view.View}. Modifies {@code localRect} to contain + * only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link android.graphics.Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor means that this view is not visible. + ViewParent viewParent = mView.getParent(); + while (viewParent instanceof View) { + final View view = (View) viewParent; + if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + viewParent = view.getParent(); + } + + // A null parent implies the view is not visible. + if (viewParent == null) { + return false; + } + + // If no portion of the parent is visible, this view is not visible. + if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + /** + * Returns whether this virtual view is accessibility focused. + * + * @return True if the view is accessibility focused. + */ + private boolean isAccessibilityFocused(int virtualViewId) { + return (mFocusedVirtualViewId == virtualViewId); + } + + /** + * Attempts to give accessibility focus to a virtual view. + * <p> + * A virtual view will not actually take focus if + * {@link android.view.accessibility.AccessibilityManager#isEnabled()} returns false, + * {@link android.view.accessibility.AccessibilityManager#isTouchExplorationEnabled()} returns false, + * or the view already has accessibility focus. + * + * @param virtualViewId The id of the virtual view on which to place + * accessibility focus. + * @return Whether this virtual view actually took accessibility focus. + */ + private boolean requestAccessibilityFocus(int virtualViewId) { + final AccessibilityManager accessibilityManager = + (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (!mManager.isEnabled() + || !accessibilityManager.isTouchExplorationEnabled()) { + return false; + } + // TODO: Check virtual view visibility. + if (!isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + } + return false; + } + + /** + * Attempts to clear accessibility focus from a virtual view. + * + * @param virtualViewId The id of the virtual view from which to clear + * accessibility focus. + * @return Whether this virtual view actually cleared accessibility focus. + */ + private boolean clearAccessibilityFocus(int virtualViewId) { + if (isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = INVALID_ID; + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + } + return false; + } + + /** + * Provides a mapping between view-relative coordinates and logical + * items. + * + * @param x The view-relative x coordinate + * @param y The view-relative y coordinate + * @return virtual view identifier for the logical item under + * coordinates (x,y) + */ + protected abstract int getVirtualViewAt(float x, float y); + + /** + * Populates a list with the view's visible items. The ordering of items + * within {@code virtualViewIds} specifies order of accessibility focus + * traversal. + * + * @param virtualViewIds The list to populate with visible items + */ + protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); + + /** + * Populates an {@link android.view.accessibility.AccessibilityEvent} with information about the + * specified item. + * <p> + * Implementations <b>must</b> populate the following required fields: + * <ul> + * <li>event text, see {@link android.view.accessibility.AccessibilityEvent#getText} or + * {@link android.view.accessibility.AccessibilityEvent#setContentDescription} + * </ul> + * <p> + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + * <ul> + * <li>item class name, set to android.view.View, see + * {@link android.view.accessibility.AccessibilityEvent#setClassName} + * </ul> + * <p> + * The following required fields are automatically populated by the + * helper class and may not be overridden: + * <ul> + * <li>package name, set to the package of the host view's + * {@link android.content.Context}, see {@link android.view.accessibility.AccessibilityEvent#setPackageName} + * <li>event source, set to the host view and virtual view identifier, + * see {@link android.view.accessibility.AccessibilityRecord#setSource(android.view.View, int)} + * </ul> + * + * @param virtualViewId The virtual view id for the item for which to + * populate the event + * @param event The event to populate + */ + protected abstract void onPopulateEventForVirtualView( + int virtualViewId, AccessibilityEvent event); + + /** + * Populates an {@link android.view.accessibility.AccessibilityNodeInfo} with information + * about the specified item. + * <p> + * Implementations <b>must</b> populate the following required fields: + * <ul> + * <li>event text, see {@link android.view.accessibility.AccessibilityNodeInfo#setText} or + * {@link android.view.accessibility.AccessibilityNodeInfo#setContentDescription} + * <li>bounds in parent coordinates, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent} + * </ul> + * <p> + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + * <ul> + * <li>enabled state, set to true, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setEnabled} + * <li>item class name, identical to the class name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setClassName} + * </ul> + * <p> + * The following required fields are automatically populated by the + * helper class and may not be overridden: + * <ul> + * <li>package name, identical to the package name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setPackageName} + * <li>node source, identical to the event source set in + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setSource(android.view.View, int)} + * <li>parent view, set to the host view, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setParent(android.view.View)} + * <li>visibility, computed based on parent-relative bounds, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>accessibility focus, computed based on internal helper state, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setAccessibilityFocused} + * <li>bounds in screen coordinates, computed based on host view bounds, + * see {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen} + * </ul> + * <p> + * Additionally, the helper class automatically handles accessibility + * focus management by adding the appropriate + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * action. Implementations must <b>never</b> manually add these actions. + * <p> + * The helper class also automatically modifies parent- and + * screen-relative bounds to reflect the portion of the item visible + * within its parent. + * + * @param virtualViewId The virtual view identifier of the item for + * which to populate the node + * @param node The node to populate + */ + protected abstract void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfo node); + + /** + * Performs the specified accessibility action on the item associated + * with the virtual view identifier. See + * {@link android.view.accessibility.AccessibilityNodeInfo#performAction(int, android.os.Bundle)} for + * more information. + * <p> + * Implementations <b>must</b> handle any actions added manually in + * {@link #onPopulateNodeForVirtualView}. + * <p> + * The helper class automatically handles focus management resulting + * from {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} + * and + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * actions. + * + * @param virtualViewId The virtual view identifier of the item on which + * to perform the action + * @param action The accessibility action to perform + * @param arguments (Optional) A bundle with additional arguments, or + * null + * @return true if the action was performed + */ + protected abstract boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments); + + /** + * Exposes a virtual view hierarchy to the accessibility framework. Only + * used in API 16+. + */ + private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return ExploreByTouchHelper.this.createNode(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); + } + } +} diff --git a/src/com/android/settings/widget/LinkAccessibilityHelper.java b/src/com/android/settings/widget/LinkAccessibilityHelper.java new file mode 100644 index 0000000..2d4d585 --- /dev/null +++ b/src/com/android/settings/widget/LinkAccessibilityHelper.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2014 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.settings.widget; + +import android.graphics.Rect; +import android.os.Bundle; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.TextView; + +import java.util.List; + +/** + * Copied from setup wizard. + */ +public class LinkAccessibilityHelper extends ExploreByTouchHelper { + + private static final String TAG = "LinkAccessibilityHelper"; + + private final TextView mView; + private final Rect mTempRect = new Rect(); + + public LinkAccessibilityHelper(TextView view) { + super(view); + mView = view; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + final int offset = mView.getOffsetForPosition(x, y); + ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (linkSpans.length == 1) { + ClickableSpan linkSpan = linkSpans[0]; + return spannedText.getSpanStart(linkSpan); + } + } + return INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), + ClickableSpan.class); + for (ClickableSpan span : linkSpans) { + virtualViewIds.add(spannedText.getSpanStart(span)); + } + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + event.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "ClickableSpan is null for offset: " + virtualViewId); + event.setContentDescription(mView.getText()); + } + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo info) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + info.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "ClickableSpan is null for offset: " + virtualViewId); + info.setContentDescription(mView.getText()); + } + info.setFocusable(true); + info.setClickable(true); + getBoundsForSpan(span, mTempRect); + if (!mTempRect.isEmpty()) { + info.setBoundsInParent(getBoundsForSpan(span, mTempRect)); + } else { + Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); + mTempRect.set(0, 0, 1, 1); + info.setBoundsInParent(mTempRect); + } + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + span.onClick(mView); + return true; + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + } + } + return false; + } + + private ClickableSpan getSpanForOffset(int offset) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (spans.length == 1) { + return spans[0]; + } + } + return null; + } + + private CharSequence getTextForSpan(ClickableSpan span) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + return spannedText.subSequence(spannedText.getSpanStart(span), + spannedText.getSpanEnd(span)); + } + return text; + } + + // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the + // section on the first line. + private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { + CharSequence text = mView.getText(); + outRect.setEmpty(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + final int spanStart = spannedText.getSpanStart(span); + final int spanEnd = spannedText.getSpanEnd(span); + final Layout layout = mView.getLayout(); + final float xStart = layout.getPrimaryHorizontal(spanStart); + final float xEnd = layout.getPrimaryHorizontal(spanEnd); + final int lineStart = layout.getLineForOffset(spanStart); + final int lineEnd = layout.getLineForOffset(spanEnd); + layout.getLineBounds(lineStart, outRect); + outRect.left = (int) xStart; + if (lineEnd == lineStart) { + outRect.right = (int) xEnd; + } // otherwise just leave it at the end of the start line + + // Offset for padding + outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); + } + return outRect; + } +} diff --git a/src/com/android/settings/widget/LinkTextView.java b/src/com/android/settings/widget/LinkTextView.java new file mode 100644 index 0000000..ab72fcf --- /dev/null +++ b/src/com/android/settings/widget/LinkTextView.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 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.settings.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * Copied from setup wizard. + */ +public class LinkTextView extends TextView { + + private LinkAccessibilityHelper mAccessibilityHelper; + + public LinkTextView(Context context) { + this(context, null); + } + + public LinkTextView(Context context, AttributeSet attrs) { + super(context, attrs); + mAccessibilityHelper = new LinkAccessibilityHelper(this); + setAccessibilityDelegate(mAccessibilityHelper); + } + + @Override + protected boolean dispatchHoverEvent(@NonNull MotionEvent event) { + if (mAccessibilityHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } +} |