While a page is scrolling to the target position, the target can't request a11y focus, and the highlight blink animation at the moment is also invisible. The patch is to skip the unseen highlight animation and focus operation when onBindViewHolder() is called before scrolling. Therefore, when the scrolling stops, the scroll listener will notify the item changed and onBindViewHolder() will be called again to do the operations. Also decrease the delay time of the scrolling start to make the a11y smoother. Bug: 318459003 Test: manual, robotest Change-Id: Ie55b2edeb7e1feaaadb5e670f39f07f6f17b92b9
280 lines
12 KiB
Java
280 lines
12 KiB
Java
/*
|
|
* Copyright (C) 2018 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.settings.widget;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyInt;
|
|
import static org.mockito.ArgumentMatchers.anyString;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.spy;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.verifyNoInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
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;
|
|
import androidx.preference.PreferenceScreen;
|
|
import androidx.preference.PreferenceViewHolder;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsActivity;
|
|
import com.android.settings.SettingsPreferenceFragment;
|
|
|
|
import com.google.android.material.appbar.AppBarLayout;
|
|
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.Mock;
|
|
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)
|
|
@Config(shadows = {
|
|
com.android.settings.testutils.shadow.ShadowFragment.class,
|
|
})
|
|
public class HighlightablePreferenceGroupAdapterTest {
|
|
|
|
private static final String TEST_KEY = "key";
|
|
|
|
@Mock
|
|
private View mRoot;
|
|
@Mock
|
|
private PreferenceCategory mPreferenceCategory;
|
|
@Mock
|
|
private SettingsPreferenceFragment mFragment;
|
|
|
|
private Context mContext;
|
|
private HighlightablePreferenceGroupAdapter mAdapter;
|
|
private PreferenceViewHolder mViewHolder;
|
|
private Preference mPreference;
|
|
|
|
@Before
|
|
public void setUp() {
|
|
MockitoAnnotations.initMocks(this);
|
|
mContext = RuntimeEnvironment.application;
|
|
mPreference = new Preference(mContext);
|
|
mPreference.setKey(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(
|
|
View.inflate(mContext, androidx.preference.R.layout.preference, null));
|
|
}
|
|
|
|
@Test
|
|
public void requestHighlight_hasKey_notHighlightedBefore_shouldRequest() {
|
|
when(mAdapter.getPreferenceAdapterPosition(anyString())).thenReturn(1);
|
|
mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
|
|
|
|
verify(mRoot).postDelayed(any(),
|
|
eq(HighlightablePreferenceGroupAdapter.DELAY_COLLAPSE_DURATION_MILLIS));
|
|
verify(mRoot).postDelayed(any(),
|
|
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);
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightRequested", false);
|
|
mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
|
|
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightKey", TEST_KEY);
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightRequested", true);
|
|
mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
|
|
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightKey", TEST_KEY);
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightRequested", false);
|
|
mAdapter.requestHighlight(mRoot, null /* recyclerView */, mock(AppBarLayout.class));
|
|
|
|
verifyNoInteractions(mRoot);
|
|
}
|
|
|
|
@Test
|
|
public void adjustInitialExpandedChildCount_invalidInput_shouldNotAdjust() {
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(null /* host */);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
final Bundle args = new Bundle();
|
|
when(mFragment.getArguments()).thenReturn(args);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
final PreferenceScreen screen = mock(PreferenceScreen.class);
|
|
when(mFragment.getArguments()).thenReturn(null);
|
|
when(mFragment.getPreferenceScreen()).thenReturn(screen);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
verifyNoInteractions(screen);
|
|
}
|
|
|
|
@Test
|
|
public void adjustInitialExpandedChildCount_hasHighlightKey_shouldExpandAllChildren() {
|
|
final Bundle args = new Bundle();
|
|
when(mFragment.getArguments()).thenReturn(args);
|
|
args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, "testkey");
|
|
final PreferenceScreen screen = mock(PreferenceScreen.class);
|
|
when(mFragment.getPreferenceScreen()).thenReturn(screen);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
|
|
verify(screen).setInitialExpandedChildrenCount(Integer.MAX_VALUE);
|
|
}
|
|
|
|
@Test
|
|
public void adjustInitialExpandedChildCount_noKeyOrChildCountOverride_shouldDoNothing() {
|
|
final Bundle args = new Bundle();
|
|
when(mFragment.getArguments()).thenReturn(args);
|
|
when(mFragment.getInitialExpandedChildCount()).thenReturn(-1);
|
|
final PreferenceScreen screen = mock(PreferenceScreen.class);
|
|
when(mFragment.getPreferenceScreen()).thenReturn(screen);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
|
|
verify(mFragment).getInitialExpandedChildCount();
|
|
verifyNoInteractions(screen);
|
|
}
|
|
|
|
@Test
|
|
public void adjustInitialExpandedChildCount_hasCountOverride_shouldDoNothing() {
|
|
when(mFragment.getInitialExpandedChildCount()).thenReturn(10);
|
|
final PreferenceScreen screen = mock(PreferenceScreen.class);
|
|
when(mFragment.getPreferenceScreen()).thenReturn(screen);
|
|
HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
|
|
|
|
verify(mFragment).getInitialExpandedChildCount();
|
|
|
|
verify(screen).setInitialExpandedChildrenCount(10);
|
|
}
|
|
|
|
@Test
|
|
public void updateBackground_notHighlightedRow_shouldNotSetHighlightedTag() {
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
|
|
|
|
mAdapter.updateBackground(mViewHolder, 0);
|
|
|
|
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);
|
|
|
|
mAdapter.updateBackground(mViewHolder, 10);
|
|
|
|
verify(viewItem).requestAccessibilityFocus();
|
|
}
|
|
|
|
@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);
|
|
|
|
assertThat(mAdapter.mFadeInAnimated).isTrue();
|
|
assertThat(mViewHolder.itemView.getBackground()).isInstanceOf(ColorDrawable.class);
|
|
assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(true);
|
|
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
|
|
// through animation.
|
|
assertThat(mAdapter.mFadeInAnimated).isTrue();
|
|
// remove highlight should be requested.
|
|
verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder);
|
|
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
|
|
mAdapter.updateBackground(mViewHolder, 10);
|
|
// only sets background color once - if it's animation this would be called many times
|
|
verify(mViewHolder.itemView).setBackgroundColor(mAdapter.mHighlightColor);
|
|
// remove highlight should be requested.
|
|
verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder);
|
|
}
|
|
|
|
@Test
|
|
public void updateBackground_reuseHighlightedRowForNormalRow_shouldResetBackgroundAndTag() {
|
|
ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
|
|
mViewHolder.itemView.setTag(R.id.preference_highlighted, true);
|
|
|
|
mAdapter.updateBackground(mViewHolder, 0);
|
|
|
|
assertThat(mViewHolder.itemView.getBackground()).isNotInstanceOf(ColorDrawable.class);
|
|
assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(false);
|
|
}
|
|
}
|