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

Commit 3f3785aa authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Fix the a11y focus problem in a long list page" into main

parents b3426fe8 3f0eefbd
Loading
Loading
Loading
Loading
+13 −9
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.accessibility.AccessibilityUtil;

import com.google.android.material.appbar.AppBarLayout;

@@ -50,6 +51,8 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
    static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
    @VisibleForTesting
    static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
    @VisibleForTesting
    static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L;
    private static final long HIGHLIGHT_DURATION = 15000L;
    private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
    private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
@@ -59,6 +62,7 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
    @VisibleForTesting
    boolean mFadeInAnimated;

    private final Context mContext;
    private final int mNormalBackgroundRes;
    private final String mHighlightKey;
    private boolean mHighlightRequested;
@@ -102,12 +106,12 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
        super(preferenceGroup);
        mHighlightKey = key;
        mHighlightRequested = highlightRequested;
        final Context context = preferenceGroup.getContext();
        mContext = preferenceGroup.getContext();
        final TypedValue outValue = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
        mContext.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
                outValue, true /* resolveRefs */);
        mNormalBackgroundRes = outValue.resourceId;
        mHighlightColor = context.getColor(R.color.preference_highlight_color);
        mHighlightColor = mContext.getColor(R.color.preference_highlight_color);
    }

    @Override
@@ -121,12 +125,11 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter
        View v = holder.itemView;
        if (position == mHighlightPosition
                && (mHighlightKey != null
                && TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
                && TextUtils.equals(mHighlightKey, getItem(position).getKey()))
                && v.isShown()) {
            // This position should be highlighted. If it's highlighted before - skip animation.
            addHighlightBackground(holder, !mFadeInAnimated);
            if (v != null) {
            v.requestAccessibilityFocus();
            }
            addHighlightBackground(holder, !mFadeInAnimated);
        } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
            // View with highlight is reused for a view that should not have highlight
            removeHighlightBackground(holder, false /* animate */);
@@ -157,13 +160,14 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter

        // Remove the animator as early as possible to avoid a RecyclerView crash.
        recyclerView.setItemAnimator(null);
        // Scroll to correct position after 600 milliseconds.
        // Scroll to correct position after a short delay.
        root.postDelayed(() -> {
            if (ensureHighlightPosition()) {
                recyclerView.smoothScrollToPosition(mHighlightPosition);
                highlightAndFocusTargetItem(recyclerView, mHighlightPosition);
            }
        }, DELAY_HIGHLIGHT_DURATION_MILLIS);
        }, AccessibilityUtil.isTouchExploreEnabled(mContext)
                ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS);
    }

    private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) {
+44 −3
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.View;
import android.view.accessibility.AccessibilityManager;

import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
@@ -54,6 +55,8 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowAccessibilityManager;
import org.robolectric.util.ReflectionHelpers;

@RunWith(RobolectricTestRunner.class)
@@ -67,7 +70,7 @@ public class HighlightablePreferenceGroupAdapterTest {
    @Mock
    private View mRoot;
    @Mock
    private PreferenceCategory mPreferenceCatetory;
    private PreferenceCategory mPreferenceCategory;
    @Mock
    private SettingsPreferenceFragment mFragment;

@@ -82,8 +85,8 @@ public class HighlightablePreferenceGroupAdapterTest {
        mContext = RuntimeEnvironment.application;
        mPreference = new Preference(mContext);
        mPreference.setKey(TEST_KEY);
        when(mPreferenceCatetory.getContext()).thenReturn(mContext);
        mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY,
        when(mPreferenceCategory.getContext()).thenReturn(mContext);
        mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCategory, TEST_KEY,
                false /* highlighted*/));
        when(mAdapter.getItem(anyInt())).thenReturn(mPreference);
        mViewHolder = PreferenceViewHolder.createInstanceForTests(
@@ -101,6 +104,18 @@ public class HighlightablePreferenceGroupAdapterTest {
                eq(HighlightablePreferenceGroupAdapter.DELAY_HIGHLIGHT_DURATION_MILLIS));
    }

    @Test
    public void requestHighlight_enableTouchExploration_shouldHaveA11yHighlightDelay() {
        ShadowAccessibilityManager am = Shadow.extract(AccessibilityManager.getInstance(mContext));
        am.setTouchExplorationEnabled(true);
        when(mAdapter.getPreferenceAdapterPosition(anyString())).thenReturn(1);
        mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));

        // DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = DELAY_COLLAPSE_DURATION_MILLIS
        verify(mRoot, times(2)).postDelayed(any(),
                eq(HighlightablePreferenceGroupAdapter.DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y));
    }

    @Test
    public void requestHighlight_noKey_highlightedBefore_noRecyclerView_shouldNotRequest() {
        ReflectionHelpers.setField(mAdapter, "mHighlightKey", null);
@@ -178,12 +193,24 @@ public class HighlightablePreferenceGroupAdapterTest {
        assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
    }

    @Test
    public void updateBackground_itemViewIsInvisible_shouldNotSetHighlightedTag() {
        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
        when(mViewHolder.itemView.isShown()).thenReturn(false);

        mAdapter.updateBackground(mViewHolder, 0);

        assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
    }

    /**
     * When background is being updated, we also request the a11y focus on the preference
     */
    @Test
    public void updateBackground_shouldRequestAccessibilityFocus() {
        View viewItem = mock(View.class);
        when(viewItem.isShown()).thenReturn(true);
        mViewHolder = PreferenceViewHolder.createInstanceForTests(viewItem);
        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);

@@ -195,6 +222,8 @@ public class HighlightablePreferenceGroupAdapterTest {
    @Test
    public void updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag() {
        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
        when(mViewHolder.itemView.isShown()).thenReturn(true);
        assertThat(mAdapter.mFadeInAnimated).isFalse();

        mAdapter.updateBackground(mViewHolder, 10);
@@ -205,10 +234,22 @@ public class HighlightablePreferenceGroupAdapterTest {
        verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder);
    }

    @Test
    public void updateBackground_highlight_itemViewIsInvisible_shouldNotAnimate() {
        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
        when(mViewHolder.itemView.isShown()).thenReturn(false);

        mAdapter.updateBackground(mViewHolder, 10);

        assertThat(mAdapter.mFadeInAnimated).isFalse();
    }

    @Test
    public void updateBackgroundTwice_highlight_shouldAnimateOnce() {
        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
        when(mViewHolder.itemView.isShown()).thenReturn(true);
        assertThat(mAdapter.mFadeInAnimated).isFalse();
        mAdapter.updateBackground(mViewHolder, 10);
        // mFadeInAnimated change from false to true - indicating background change is scheduled