/* * 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 org.holoeverywhere.widget.datetimepicker; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import com.nineoldandroids.view.ViewHelper; import java.util.LinkedList; import java.util.List; public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat { /** * Virtual node identifier value for invalid nodes. */ public static final int INVALID_ID = Integer.MIN_VALUE; 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]; private final AccessibilityManager mManager; private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() { @Override public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(view, event); event.setClassName(view.getClass().getName()); } @Override public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(view, info); info.setClassName(view.getClass().getName()); } @Override public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { return TouchExplorationHelper.this; } }; private View mParentView; private int mFocusedItemId = INVALID_ID; private T mCurrentItem = null; private Object mOnHoverListener; /** * Constructs a new touch exploration helper. * * @param context The parent context. */ public TouchExplorationHelper(Context context, View parentView) { mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); mParentView = parentView; } /** * @return The current accessibility focused item, or {@code null} if no * item is focused. */ public T getFocusedItem() { return getItemForId(mFocusedItemId); } /** * Requests accessibility focus be placed on the specified item. * * @param item The item to place focus on. */ public void setFocusedItem(T item) { final int itemId = getIdForItem(item); if (itemId == INVALID_ID) { return; } performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); } /** * Clears the current accessibility focused item. */ public void clearFocusedItem() { final int itemId = mFocusedItemId; if (itemId == INVALID_ID) { return; } performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); } /** * Invalidates cached information about the parent view. * <p> * You <b>must</b> call this method after adding or removing items from the * parent view. * </p> */ public void invalidateParent() { mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } /** * Invalidates cached information for a particular item. * <p> * You <b>must</b> call this method when any of the properties set in * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have * changed. * </p> * * @param item */ public void invalidateItem(T item) { sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); } /** * Populates an event of the specified type with information about an item * and attempts to send it up through the view hierarchy. * * @param item The item for which to send an event. * @param eventType The type of event to send. * @return {@code true} if the event was sent successfully. */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public boolean sendEventForItem(T item, int eventType) { if (!mManager.isEnabled() || Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { return false; } final AccessibilityEvent event = getEventForItem(item, eventType); final ViewGroup group = (ViewGroup) mParentView.getParent(); return group.requestSendAccessibilityEvent(mParentView, event); } @Override public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { if (virtualViewId == View.NO_ID) { return getNodeForParent(); } final T item = getItemForId(virtualViewId); if (item == null) { return null; } final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); populateNodeForItemInternal(item, node); return node; } @Override public boolean performAction(int virtualViewId, int action, Bundle arguments) { if (virtualViewId == View.NO_ID) { return ViewCompat.performAccessibilityAction(mParentView, action, arguments); } final T item = getItemForId(virtualViewId); if (item == null) { return false; } boolean handled = false; switch (action) { case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: if (mFocusedItemId != virtualViewId) { mFocusedItemId = virtualViewId; sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); handled = true; } break; case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: if (mFocusedItemId == virtualViewId) { mFocusedItemId = INVALID_ID; sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); handled = true; } break; } handled |= performActionForItem(item, action, arguments); return handled; } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public View.OnHoverListener getOnHoverListener() { if (mOnHoverListener == null) { mOnHoverListener = new View.OnHoverListener() { @Override public boolean onHover(View view, MotionEvent event) { if (!mManager.isTouchExplorationEnabled()) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: final T item = getItemAt(event.getX(), event.getY()); setCurrentItem(item); return true; case MotionEvent.ACTION_HOVER_EXIT: setCurrentItem(null); return true; } return false; } }; } return (View.OnHoverListener) mOnHoverListener; } private void setCurrentItem(T item) { if (mCurrentItem == item) { return; } if (mCurrentItem != null) { sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); } mCurrentItem = item; if (mCurrentItem != null) { sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); } } private AccessibilityEvent getEventForItem(T item, int eventType) { final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); final int virtualDescendantId = getIdForItem(item); // Ensure the client has good defaults. event.setEnabled(true); // Allow the client to populate the event. populateEventForItem(item, event); if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) { throw new RuntimeException( "You must add text or a content description in populateEventForItem()"); } // Don't allow the client to override these properties. event.setClassName(item.getClass().getName()); event.setPackageName(mParentView.getContext().getPackageName()); record.setSource(mParentView, virtualDescendantId); return event; } private AccessibilityNodeInfoCompat getNodeForParent() { final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView); ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info); final LinkedList<T> items = new LinkedList<T>(); getVisibleItems(items); for (T item : items) { final int virtualDescendantId = getIdForItem(item); info.addChild(mParentView, virtualDescendantId); } return info; } private AccessibilityNodeInfoCompat populateNodeForItemInternal( T item, AccessibilityNodeInfoCompat node) { final int virtualDescendantId = getIdForItem(item); // Ensure the client has good defaults. node.setEnabled(true); // Allow the client to populate the node. populateNodeForItem(item, node); if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) { throw new RuntimeException( "You must add text or a content description in populateNodeForItem()"); } // Don't allow the client to override these properties. node.setPackageName(mParentView.getContext().getPackageName()); node.setClassName(item.getClass().getName()); node.setParent(mParentView); node.setSource(mParentView, virtualDescendantId); if (mFocusedItemId == virtualDescendantId) { node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } else { node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); } node.getBoundsInParent(mTempParentRect); if (mTempParentRect.isEmpty()) { throw new RuntimeException("You must set parent bounds in populateNodeForItem()"); } // Set the visibility based on the parent bound. if (intersectVisibleToUser(mTempParentRect)) { node.setVisibleToUser(true); node.setBoundsInParent(mTempParentRect); } // Calculate screen-relative bound. mParentView.getLocationOnScreen(mTempGlobalRect); final int offsetX = mTempGlobalRect[0]; final int offsetY = mTempGlobalRect[1]; mTempScreenRect.set(mTempParentRect); mTempScreenRect.offset(offsetX, offsetY); node.setBoundsInScreen(mTempScreenRect); return node; } /** * Computes whether the specified {@link Rect} intersects with the visible * portion of its parent {@link View}. Modifies {@code localRect} to * contain only the visible portion. * * @param localRect A rectangle in local (parent) coordinates. * @return Whether the specified {@link 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 (mParentView.getWindowVisibility() != View.VISIBLE) { return false; } // An invisible predecessor or one with alpha zero means // that this view is not visible to the user. Object current = this; while (current instanceof View) { final View view = (View) current; // We have attach info so this view is attached and there is no // need to check whether we reach to ViewRootImpl on the way up. if ((ViewHelper.getAlpha(view) <= 0) || (view.getVisibility() != View.VISIBLE)) { return false; } current = view.getParent(); } // If no portion of the parent is visible, this view is not visible. if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) { return false; } // Check if the view intersects the visible portion of the parent. return localRect.intersect(mTempVisibleRect); } public AccessibilityDelegateCompat getAccessibilityDelegate() { return mDelegate; } /** * Performs an accessibility action on the specified item. See * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}. * <p> * The helper class automatically handles focus management resulting from * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so * typically a developer only needs to handle actions added manually in the * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} * method. * </p> * * @param item The item on which to perform the action. * @param action The accessibility action to perform. * @param arguments Arguments for the action, or optionally {@code null}. * @return {@code true} if the action was performed successfully. */ protected abstract boolean performActionForItem(T item, int action, Bundle arguments); /** * Populates an event with information about the specified item. * <p> * At a minimum, a developer must populate the event text by doing one of * the following: * <ul> * <li>appending text to {@link AccessibilityEvent#getText()}</li> * <li>populating a description with * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li> * </ul> * </p> * * @param item The item for which to populate the event. * @param event The event to populate. */ protected abstract void populateEventForItem(T item, AccessibilityEvent event); /** * Populates a node with information about the specified item. * <p/> * At a minimum, a developer must: * <ul> * <li>populate the event text using * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)} * </li> * <li>set the item's parent-relative bounds using * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)} * </ul> * * @param item The item for which to populate the node. * @param node The node to populate. */ protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node); /** * Populates a list with the parent view's visible items. * <p> * The result of this method is cached until the developer calls * {@link #invalidateParent()}. * </p> * * @param items The list to populate with visible items. */ protected abstract void getVisibleItems(List<T> items); /** * Returns the item under the specified parent-relative coordinates. * * @param x The parent-relative x coordinate. * @param y The parent-relative y coordinate. * @return The item under coordinates (x,y). */ protected abstract T getItemAt(float x, float y); /** * Returns the unique identifier for an item. If the specified item does not * exist, returns {@link #INVALID_ID}. * <p> * This result of this method must be consistent with * {@link #getItemForId(int)}. * </p> * * @param item The item whose identifier to return. * @return A unique identifier, or {@link #INVALID_ID}. */ protected abstract int getIdForItem(T item); /** * Returns the item for a unique identifier. If the specified item does not * exist, returns {@code null}. * * @param id The identifier for the item to return. * @return An item, or {@code null}. */ protected abstract T getItemForId(int id); }