diff options
author | 2019-08-16 08:13:39 +0000 | |
---|---|---|
committer | 2019-08-16 08:13:39 +0000 | |
commit | e6771c2cb8bc8eb4e6cdf596e6f67a4a84d3f20b (patch) | |
tree | a8f2e70c6fb19f4f5dc6c59130ae46518eea69c0 | |
parent | f4e96eb8197b172b1b06909e8cf9cf8075fbcbe7 (diff) | |
parent | f6bf0054cf471616bcf2dc715cfeeca411686a70 (diff) |
Merge "Fix broken a11y function"
6 files changed, 109 insertions, 59 deletions
diff --git a/res/layout/fixed_layout.xml b/res/layout/fixed_layout.xml index 7f7547fd4..4116df614 100644 --- a/res/layout/fixed_layout.xml +++ b/res/layout/fixed_layout.xml @@ -57,7 +57,7 @@ android:id="@+id/horizontal_breadcrumb" android:layout_marginRight="20dp" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="wrap_content" /> </androidx.appcompat.widget.Toolbar> diff --git a/res/layout/navigation_breadcrumb_item.xml b/res/layout/navigation_breadcrumb_item.xml index 1dd6d921f..ef7977238 100644 --- a/res/layout/navigation_breadcrumb_item.xml +++ b/res/layout/navigation_breadcrumb_item.xml @@ -40,7 +40,7 @@ android:layout_height="match_parent" android:gravity="center_vertical" android:duplicateParentState="true" - android:textAppearance="@android:style/TextAppearance.Material.Widget.ActionBar.Title" + android:textAppearance="@style/ToolbarTitle" android:background="@drawable/breadcrumb_item_background" /> <ImageView diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java index 6a7cce1ff..91480dbb5 100644 --- a/src/com/android/documentsui/HorizontalBreadcrumb.java +++ b/src/com/android/documentsui/HorizontalBreadcrumb.java @@ -71,14 +71,8 @@ public final class HorizontalBreadcrumb extends RecyclerView mLayoutManager = new LinearLayoutManager( getContext(), LinearLayoutManager.HORIZONTAL, false); mAdapter = new BreadcrumbAdapter( - state, env, new ItemDragListener<>(this), this::onKey); - // Since we are using GestureDetector to detect click events, a11y services don't know which views - // are clickable because we aren't using View.OnClickListener. Thus, we need to use a custom - // accessibility delegate to route click events correctly. See AccessibilityClickEventRouter - // for more details on how we are routing these a11y events. - setAccessibilityDelegateCompat( - new AccessibilityEventRouter(this, - (View child) -> onAccessibilityClick(child), null)); + state, env, new ItemDragListener<>(this), this::onKey, + new AccessibilityEventRouter(this::onAccessibilityClick, null)); setLayoutManager(mLayoutManager); addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp)); @@ -192,17 +186,20 @@ public final class HorizontalBreadcrumb extends RecyclerView private final com.android.documentsui.base.State mState; private final OnDragListener mDragListener; private final View.OnKeyListener mClickListener; + private final View.AccessibilityDelegate mAccessibilityDelegate; // We keep the old item size so the breadcrumb will only re-render views that are necessary private int mLastItemSize; public BreadcrumbAdapter(com.android.documentsui.base.State state, Environment env, OnDragListener dragListener, - View.OnKeyListener clickListener) { + View.OnKeyListener clickListener, + View.AccessibilityDelegate accessibilityDelegate) { mState = state; mEnv = env; mDragListener = dragListener; mClickListener = clickListener; + mAccessibilityDelegate = accessibilityDelegate; mLastItemSize = mState.stack.size(); } @@ -235,6 +232,12 @@ public final class HorizontalBreadcrumb extends RecyclerView } holder.itemView.setOnDragListener(mDragListener); holder.itemView.setOnKeyListener(mClickListener); + // Since we are using GestureDetector to detect click events, a11y services don't know + // which views are clickable because we aren't using View.OnClickListener. Thus, we + // need to use a custom accessibility delegate to route click events correctly. + // See AccessibilityClickEventRouter for more details on how we are routing these + // a11y events. + holder.itemView.setAccessibilityDelegate(mAccessibilityDelegate); } private DocumentInfo getItem(int position) { diff --git a/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java b/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java index e12bca1db..51a55b417 100644 --- a/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java +++ b/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java @@ -18,19 +18,15 @@ package com.android.documentsui.dirlist; import android.os.Bundle; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.view.AccessibilityDelegateCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import java.util.function.Function; /** - * Custom Accessibility Delegate for RecyclerViews to route click events on its child views to + * Custom Accessibility Delegate for item views in RecyclerViews to route click events to * proper handlers, and to surface selection state to a11y events. * <p> * The majority of event handling isdone using TouchDetector instead of View.OnCLickListener, which @@ -43,59 +39,41 @@ import java.util.function.Function; * for marking a view as selected. We will surface that selection state to a11y services in this * class. */ -public class AccessibilityEventRouter extends RecyclerViewAccessibilityDelegate { +public class AccessibilityEventRouter extends View.AccessibilityDelegate { - private final ItemDelegate mItemDelegate; private final Function<View, Boolean> mClickCallback; private final Function<View, Boolean> mLongClickCallback; public AccessibilityEventRouter( - RecyclerView recyclerView, @NonNull Function<View, Boolean> clickCallback, + @NonNull Function<View, Boolean> clickCallback, @Nullable Function<View, Boolean> longClickCallback) { - super(recyclerView); + super(); mClickCallback = clickCallback; mLongClickCallback = longClickCallback; - mItemDelegate = new ItemDelegate(this) { - @Override - public void onInitializeAccessibilityNodeInfo(View host, - AccessibilityNodeInfoCompat info) { - super.onInitializeAccessibilityNodeInfo(host, info); - final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(host); - // if the viewHolder is a DocumentsHolder instance and the ItemDetails - // is null, it can't be clicked - if (holder instanceof DocumentHolder) { - if (((DocumentHolder)holder).getItemDetails() != null) { - addAction(info); - } - } else { - addAction(info); - } - info.setSelected(host.isActivated()); - } + } - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - // We are only handling click events; route all other to default implementation - if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { - return mClickCallback.apply(host); - } else if (action == AccessibilityNodeInfoCompat.ACTION_LONG_CLICK - && mLongClickCallback != null) { - return mLongClickCallback.apply(host); - } - return super.performAccessibilityAction(host, action, args); - } - }; + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + addAction(info); + info.setSelected(host.isActivated()); } @Override - public AccessibilityDelegateCompat getItemDelegate() { - return mItemDelegate; + public boolean performAccessibilityAction(View host, int action, Bundle args) { + // We are only handling click events; route all other to default implementation + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + return mClickCallback.apply(host); + } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK + && mLongClickCallback != null) { + return mLongClickCallback.apply(host); + } + return super.performAccessibilityAction(host, action, args); } - private void addAction(AccessibilityNodeInfoCompat info) { - info.addAction(AccessibilityActionCompat.ACTION_CLICK); + private void addAction(AccessibilityNodeInfo info) { + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); if (mLongClickCallback != null) { - info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); + info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); } } }
\ No newline at end of file diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index 1ff364701..18d618c79 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -325,10 +325,6 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mFocusManager = mInjector.getFocusManager(mRecView, mModel); mActions = mInjector.getActionHandler(mContentLock); - mRecView.setAccessibilityDelegateCompat( - new AccessibilityEventRouter(mRecView, - (View child) -> onAccessibilityClick(child), - (View child) -> onAccessibilityLongClick(child))); mSelectionMetadata = new SelectionMetadata(mModel::getItem); mDetailsLookup = new DocsItemDetailsLookup(mRecView); @@ -1270,6 +1266,10 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On @Override public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) { setupDragAndDropOnDocumentView(holder.itemView, cursor); + holder.itemView.setAccessibilityDelegate(new AccessibilityEventRouter( + DirectoryFragment.this::onAccessibilityClick, + DirectoryFragment.this::onAccessibilityLongClick + )); } @Override diff --git a/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java b/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java new file mode 100644 index 000000000..99d4751a6 --- /dev/null +++ b/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 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.documentsui.dirlist; + +import android.test.AndroidTestCase; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.test.filters.SmallTest; + +import com.android.documentsui.testing.Views; + +@SmallTest +public class AccessibilityTest extends AndroidTestCase { + + private AccessibilityEventRouter mAccessibilityDelegate; + private boolean mClickCallbackCalled = false; + private boolean mLongClickCallbackCalled = false; + + @Override + public void setUp() throws Exception { + mAccessibilityDelegate = new AccessibilityEventRouter((View v) -> { + mClickCallbackCalled = true; + return true; + }, (View v) -> { + mLongClickCallbackCalled = true; + return true; + }); + } + + public void test_announceSelected() throws Exception { + View item = Views.createTestView(true); + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(item, info); + assertTrue(info.isSelected()); + } + + public void test_routesAccessibilityClicks() throws Exception { + View item = Views.createTestView(true); + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(item, info); + mAccessibilityDelegate.performAccessibilityAction( + item, AccessibilityNodeInfo.ACTION_CLICK, null); + assertTrue(mClickCallbackCalled); + } + + public void test_routesAccessibilityLongClicks() throws Exception { + View item = Views.createTestView(true); + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(item, info); + mAccessibilityDelegate.performAccessibilityAction( + item, AccessibilityNodeInfo.ACTION_LONG_CLICK, null); + assertTrue(mLongClickCallbackCalled); + } +} |