diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java index 7145460d460..82ef58bb145 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -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. + v.requestAccessibilityFocus(); addHighlightBackground(holder, !mFadeInAnimated); - if (v != null) { - v.requestAccessibilityFocus(); - } } 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) { diff --git a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java index 29560ab88eb..03684ad88a5 100644 --- a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java +++ b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java @@ -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