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

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

Manually routing Accessibility clicks for RecyclerView classes.

DocsUI uses TouchDetector to differentiate mouse/gesture events; this
prevents a11y services to know what logic to run when there's a
ACCESSIBILITY_CLICK event. This CL manually adds these accessibility
click events to child views, and also route these to correct click
callbacks.

Test: Manually done
Bug: 32412100
Bug: 30613053
Change-Id: If3bf2a039b3cb269e32555d1740f0420cfa50b93
(cherry picked from commit 62442459)
parent 82e6c91e
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;

import java.util.function.Consumer;
import java.util.function.IntConsumer;

/**
 * Dropdown implementation of breadcrumb used for phone device layouts
@@ -60,7 +60,7 @@ public final class DropdownBreadcrumb extends Spinner implements Breadcrumb {
    }

    @Override
    public void setup(Environment env, State state, Consumer<Integer> listener) {
    public void setup(Environment env, State state, IntConsumer listener) {
        mAdapter = new DropdownAdapter(state, env);
        setOnItemSelectedListener(
                new OnItemSelectedListener() {
+23 −5
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ 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 java.util.function.Consumer;
import java.util.function.IntConsumer;

/**
 * Horizontal implementation of breadcrumb used for tablet / desktop device layouts
@@ -44,7 +46,7 @@ public final class HorizontalBreadcrumb extends RecyclerView

    private LinearLayoutManager mLayoutManager;
    private BreadcrumbAdapter mAdapter;
    private Consumer<Integer> mListener;
    private IntConsumer mClickListener;

    public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
@@ -61,13 +63,20 @@ public final class HorizontalBreadcrumb extends RecyclerView
    @Override
    public void setup(Environment env,
            com.android.documentsui.base.State state,
            Consumer<Integer> listener) {
            IntConsumer listener) {

        mListener = listener;
        mClickListener = listener;
        mLayoutManager = new LinearLayoutManager(
                getContext(), LinearLayoutManager.HORIZONTAL, false);
        mAdapter = new BreadcrumbAdapter(
                state, env, new ItemDragListener<>(this));
        // 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 AccessibilityClickEventRouter(this,
                        (View child) -> onAccessibilityClick(child)));

        setLayoutManager(mLayoutManager);
        addOnItemTouchListener(new ClickListener(getContext(), this::onSingleTapUp));
@@ -108,6 +117,15 @@ public final class HorizontalBreadcrumb extends RecyclerView
        return (maxOffset - computeHorizontalScrollOffset() > USER_NO_SCROLL_OFFSET_THRESHOLD);
    }

    private boolean onAccessibilityClick(View child) {
        int pos = getChildAdapterPosition(child);
        if (pos != getAdapter().getItemCount() - 1) {
            mClickListener.accept(pos);
            return true;
        }
        return false;
    }

    @Override
    public void postUpdate() {
    }
@@ -139,7 +157,7 @@ public final class HorizontalBreadcrumb extends RecyclerView
    public void onViewHovered(View v) {
        int pos = getChildAdapterPosition(v);
        if (pos != mAdapter.getItemCount() - 1) {
            mListener.accept(pos);
            mClickListener.accept(pos);
        }
    }

@@ -147,7 +165,7 @@ public final class HorizontalBreadcrumb extends RecyclerView
        View itemView = findChildViewUnder(e.getX(), e.getY());
        int pos = getChildAdapterPosition(itemView);
        if (pos != mAdapter.getItemCount() - 1) {
            mListener.accept(pos);
            mClickListener.accept(pos);
        }
    }

+2 −2
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.AnimationView;

import java.util.function.Consumer;
import java.util.function.IntConsumer;

/**
 * A facade over the portions of the app and drawer toolbars.
@@ -124,7 +124,7 @@ public class NavigationViewManager {
    }

    interface Breadcrumb {
        void setup(Environment env, State state, Consumer<Integer> listener);
        void setup(Environment env, State state, IntConsumer listener);
        void show(boolean visibility);
        void postUpdate();
    }
+71 −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.os.Bundle;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
import android.view.View;

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
 * click callbacks.
 */
public class AccessibilityClickEventRouter extends RecyclerViewAccessibilityDelegate {

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

    public AccessibilityClickEventRouter(
            RecyclerView recyclerView, Function<View, Boolean> clickCallback) {
        super(recyclerView);
        mClickCallback = clickCallback;
        mItemDelegate = new ItemDelegate(this) {
            @Override
            public void onInitializeAccessibilityNodeInfo(View host,
                    AccessibilityNodeInfoCompat info) {
                super.onInitializeAccessibilityNodeInfo(host, info);
                info.addAction(AccessibilityActionCompat.ACTION_CLICK);
            }

            @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);
                }
                return super.performAccessibilityAction(host, action, args);
            }
        };
    }

    @Override
    public AccessibilityDelegateCompat getItemDelegate() {
        return mItemDelegate;
    }
}
 No newline at end of file
+9 −0
Original line number Diff line number Diff line
@@ -306,6 +306,9 @@ public class DirectoryFragment extends Fragment
        mFocusManager = mInjector.getFocusManager(mRecView, mModel);
        mActions = mInjector.getActionHandler(mModel);

        mRecView.setAccessibilityDelegateCompat(
                new AccessibilityClickEventRouter(mRecView,
                        (View child) -> onAccessibilityClick(child)));
        mSelectionMetadata = new SelectionMetadata(mModel::getItem);
        mSelectionMgr.addItemCallback(mSelectionMetadata);

@@ -686,6 +689,12 @@ public class DirectoryFragment extends Fragment
        return false;
    }

    private boolean onAccessibilityClick(View child) {
        DocumentDetails doc = getDocumentHolder(child);
        mActions.openDocument(doc);
        return true;
    }

    private void cancelThumbnailTask(View view) {
        final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
        if (iconThumb != null) {