Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a355d190 authored by Ben Lin's avatar Ben Lin
Browse files

Communicate selection state to a11y services.

DocumentsUI uses View#setActivated instead of View#setSelected for its
selction states, because setActivated propagates downward in the view
hierarchy (i.e. updates the state on its child views), while setSelected
does not. a11y services uses View#isSelected to communication selection
state. Thus, we attach the information to the accessibilityNodeInfo for
each RecyclerView item.

Bug: 35322527
Change-Id: I734fe7c00cfc8f2e57eadb519a09067097191d7a
parent 97e21013
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import com.android.documentsui.NavigationViewManager.Breadcrumb;
import com.android.documentsui.NavigationViewManager.Environment;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.AccessibilityClickEventRouter;
import com.android.documentsui.dirlist.AccessibilityEventRouter;

import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -76,7 +76,7 @@ public final class HorizontalBreadcrumb extends RecyclerView
        // accessibility delegate to route click events correctly. See AccessibilityClickEventRouter
        // for more details on how we are routing these a11y events.
        setAccessibilityDelegateCompat(
                new AccessibilityClickEventRouter(this,
                new AccessibilityEventRouter(this,
                        (View child) -> onAccessibilityClick(child)));

        setLayoutManager(mLayoutManager);
+13 −8
Original line number Diff line number Diff line
@@ -28,20 +28,24 @@ import java.util.function.Function;

/**
 * Custom Accessibility Delegate for RecyclerViews to route click events on its child views to
 * proper handlers.
 *
 * The majority of event handling is done using TouchDetector instead of View.OnCLickListener,
 * which most a11y services use to understand whether a particular view is clickable or not.
 * Thus, we need to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child
 * views' accessibility node, and then correctly route these clicks done by a11y services to responsible
 * proper handlers, and to surface selection state to a11y events.
 * <p>
 * The majority of event handling isdone using TouchDetector instead of View.OnCLickListener, which
 * most a11y services use to understand whether a particular view is clickable or not. Thus, we need
 * to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child views'
 * accessibility node, and then correctly route these clicks done by a11y services to responsible
 * click callbacks.
 * <p>
 * DocumentsUI uses {@link View#setActivated(boolean)} instead of {@link View#setSelected(boolean)}
 * for marking a view as selected. We will surface that selection state to a11y services in this
 * class.
 */
public class AccessibilityClickEventRouter extends RecyclerViewAccessibilityDelegate {
public class AccessibilityEventRouter extends RecyclerViewAccessibilityDelegate {

    private final ItemDelegate mItemDelegate;
    private final Function<View, Boolean> mClickCallback;

    public AccessibilityClickEventRouter(
    public AccessibilityEventRouter(
            RecyclerView recyclerView, Function<View, Boolean> clickCallback) {
        super(recyclerView);
        mClickCallback = clickCallback;
@@ -51,6 +55,7 @@ public class AccessibilityClickEventRouter extends RecyclerViewAccessibilityDele
                    AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.addAction(AccessibilityActionCompat.ACTION_CLICK);
                info.setSelected(host.isActivated());
            }

            @Override
+1 −1
Original line number Diff line number Diff line
@@ -315,7 +315,7 @@ public class DirectoryFragment extends Fragment
        mActions = mInjector.getActionHandler(mModel);

        mRecView.setAccessibilityDelegateCompat(
                new AccessibilityClickEventRouter(mRecView,
                new AccessibilityEventRouter(mRecView,
                        (View child) -> onAccessibilityClick(child)));
        mSelectionMetadata = new SelectionMetadata(mModel::getItem);
        mSelectionMgr.addItemCallback(mSelectionMetadata);
+7 −0
Original line number Diff line number Diff line
@@ -44,6 +44,13 @@ public final class Views {
        return view;
    }

    public static View createTestView(boolean activated) {
        View view = createTestView();
        Mockito.when(view.isActivated()).thenReturn(activated);

        return view;
    }

    public static void setBackground(View testView, Drawable background) {
        Mockito.when(testView.getBackground()).thenReturn(background);
    }
+63 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

import com.android.documentsui.testing.TestRecyclerView;
import com.android.documentsui.testing.Views;

import java.util.List;

@SmallTest
public class AccessibilityTest extends AndroidTestCase {

    private static final List<String> ITEMS = TestData.create(10);

    private TestRecyclerView mView;
    private AccessibilityEventRouter mAccessibilityDelegate;
    private boolean mCallbackCalled = false;

    @Override
    public void setUp() throws Exception {
        mView = TestRecyclerView.create(ITEMS);
        mAccessibilityDelegate = new AccessibilityEventRouter(mView, (View v) -> {
            mCallbackCalled = true;
            return true;
        });
        mView.setAccessibilityDelegateCompat(mAccessibilityDelegate);
    }

    public void test_announceSelected() throws Exception {
        View item = Views.createTestView(true);
        AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(AccessibilityNodeInfo.obtain());
        mAccessibilityDelegate.getItemDelegate().onInitializeAccessibilityNodeInfo(item, info);
        assertTrue(info.isSelected());
    }

    public void test_routesAccessibilityClicks() throws Exception {
        View item = Views.createTestView(true);
        AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(AccessibilityNodeInfo.obtain());
        mAccessibilityDelegate.getItemDelegate().onInitializeAccessibilityNodeInfo(item, info);
        mAccessibilityDelegate.getItemDelegate().performAccessibilityAction(item, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
        assertTrue(mCallbackCalled);
    }
}