1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.widget;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.anyInt;
23 import static org.mockito.ArgumentMatchers.anyString;
24 import static org.mockito.ArgumentMatchers.eq;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.spy;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.verifyNoInteractions;
30 import static org.mockito.Mockito.when;
31 
32 import android.content.Context;
33 import android.graphics.drawable.ColorDrawable;
34 import android.os.Bundle;
35 import android.view.View;
36 import android.view.accessibility.AccessibilityManager;
37 
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceCategory;
40 import androidx.preference.PreferenceScreen;
41 import androidx.preference.PreferenceViewHolder;
42 import androidx.recyclerview.widget.RecyclerView;
43 
44 import com.android.settings.R;
45 import com.android.settings.SettingsActivity;
46 import com.android.settings.SettingsPreferenceFragment;
47 
48 import com.google.android.material.appbar.AppBarLayout;
49 
50 import org.junit.Before;
51 import org.junit.Test;
52 import org.junit.runner.RunWith;
53 import org.mockito.Mock;
54 import org.mockito.MockitoAnnotations;
55 import org.robolectric.RobolectricTestRunner;
56 import org.robolectric.RuntimeEnvironment;
57 import org.robolectric.annotation.Config;
58 import org.robolectric.shadow.api.Shadow;
59 import org.robolectric.shadows.ShadowAccessibilityManager;
60 import org.robolectric.util.ReflectionHelpers;
61 
62 @RunWith(RobolectricTestRunner.class)
63 @Config(shadows = {
64         com.android.settings.testutils.shadow.ShadowFragment.class,
65 })
66 public class HighlightablePreferenceGroupAdapterTest {
67 
68     private static final String TEST_KEY = "key";
69 
70     @Mock
71     private View mRoot;
72     @Mock
73     private PreferenceCategory mPreferenceCategory;
74     @Mock
75     private SettingsPreferenceFragment mFragment;
76 
77     private Context mContext;
78     private HighlightablePreferenceGroupAdapter mAdapter;
79     private PreferenceViewHolder mViewHolder;
80     private Preference mPreference;
81 
82     @Before
setUp()83     public void setUp() {
84         MockitoAnnotations.initMocks(this);
85         mContext = RuntimeEnvironment.application;
86         mPreference = new Preference(mContext);
87         mPreference.setKey(TEST_KEY);
88         when(mPreferenceCategory.getContext()).thenReturn(mContext);
89         mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCategory, TEST_KEY,
90                 false /* highlighted*/));
91         when(mAdapter.getItem(anyInt())).thenReturn(mPreference);
92         mViewHolder = PreferenceViewHolder.createInstanceForTests(
93                 View.inflate(mContext, androidx.preference.R.layout.preference, null));
94     }
95 
96     @Test
requestHighlight_hasKey_notHighlightedBefore_shouldRequest()97     public void requestHighlight_hasKey_notHighlightedBefore_shouldRequest() {
98         when(mAdapter.getPreferenceAdapterPosition(anyString())).thenReturn(1);
99         mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
100 
101         verify(mRoot).postDelayed(any(),
102                 eq(HighlightablePreferenceGroupAdapter.DELAY_COLLAPSE_DURATION_MILLIS));
103         verify(mRoot).postDelayed(any(),
104                 eq(HighlightablePreferenceGroupAdapter.DELAY_HIGHLIGHT_DURATION_MILLIS));
105     }
106 
107     @Test
requestHighlight_enableTouchExploration_shouldHaveA11yHighlightDelay()108     public void requestHighlight_enableTouchExploration_shouldHaveA11yHighlightDelay() {
109         ShadowAccessibilityManager am = Shadow.extract(AccessibilityManager.getInstance(mContext));
110         am.setTouchExplorationEnabled(true);
111         when(mAdapter.getPreferenceAdapterPosition(anyString())).thenReturn(1);
112         mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
113 
114         // DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = DELAY_COLLAPSE_DURATION_MILLIS
115         verify(mRoot, times(2)).postDelayed(any(),
116                 eq(HighlightablePreferenceGroupAdapter.DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y));
117     }
118 
119     @Test
requestHighlight_noKey_highlightedBefore_noRecyclerView_shouldNotRequest()120     public void requestHighlight_noKey_highlightedBefore_noRecyclerView_shouldNotRequest() {
121         ReflectionHelpers.setField(mAdapter, "mHighlightKey", null);
122         ReflectionHelpers.setField(mAdapter, "mHighlightRequested", false);
123         mAdapter.requestHighlight(mRoot, mock(RecyclerView.class),  mock(AppBarLayout.class));
124 
125         ReflectionHelpers.setField(mAdapter, "mHighlightKey", TEST_KEY);
126         ReflectionHelpers.setField(mAdapter, "mHighlightRequested", true);
127         mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
128 
129         ReflectionHelpers.setField(mAdapter, "mHighlightKey", TEST_KEY);
130         ReflectionHelpers.setField(mAdapter, "mHighlightRequested", false);
131         mAdapter.requestHighlight(mRoot, null /* recyclerView */,  mock(AppBarLayout.class));
132 
133         verifyNoInteractions(mRoot);
134     }
135 
136     @Test
adjustInitialExpandedChildCount_invalidInput_shouldNotAdjust()137     public void adjustInitialExpandedChildCount_invalidInput_shouldNotAdjust() {
138         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(null /* host */);
139         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
140         final Bundle args = new Bundle();
141         when(mFragment.getArguments()).thenReturn(args);
142         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
143         final PreferenceScreen screen = mock(PreferenceScreen.class);
144         when(mFragment.getArguments()).thenReturn(null);
145         when(mFragment.getPreferenceScreen()).thenReturn(screen);
146         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
147         verifyNoInteractions(screen);
148     }
149 
150     @Test
adjustInitialExpandedChildCount_hasHighlightKey_shouldExpandAllChildren()151     public void adjustInitialExpandedChildCount_hasHighlightKey_shouldExpandAllChildren() {
152         final Bundle args = new Bundle();
153         when(mFragment.getArguments()).thenReturn(args);
154         args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, "testkey");
155         final PreferenceScreen screen = mock(PreferenceScreen.class);
156         when(mFragment.getPreferenceScreen()).thenReturn(screen);
157         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
158 
159         verify(screen).setInitialExpandedChildrenCount(Integer.MAX_VALUE);
160     }
161 
162     @Test
adjustInitialExpandedChildCount_noKeyOrChildCountOverride_shouldDoNothing()163     public void adjustInitialExpandedChildCount_noKeyOrChildCountOverride_shouldDoNothing() {
164         final Bundle args = new Bundle();
165         when(mFragment.getArguments()).thenReturn(args);
166         when(mFragment.getInitialExpandedChildCount()).thenReturn(-1);
167         final PreferenceScreen screen = mock(PreferenceScreen.class);
168         when(mFragment.getPreferenceScreen()).thenReturn(screen);
169         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
170 
171         verify(mFragment).getInitialExpandedChildCount();
172         verifyNoInteractions(screen);
173     }
174 
175     @Test
adjustInitialExpandedChildCount_hasCountOverride_shouldDoNothing()176     public void adjustInitialExpandedChildCount_hasCountOverride_shouldDoNothing() {
177         when(mFragment.getInitialExpandedChildCount()).thenReturn(10);
178         final PreferenceScreen screen = mock(PreferenceScreen.class);
179         when(mFragment.getPreferenceScreen()).thenReturn(screen);
180         HighlightablePreferenceGroupAdapter.adjustInitialExpandedChildCount(mFragment);
181 
182         verify(mFragment).getInitialExpandedChildCount();
183 
184         verify(screen).setInitialExpandedChildrenCount(10);
185     }
186 
187     @Test
updateBackground_notHighlightedRow_shouldNotSetHighlightedTag()188     public void updateBackground_notHighlightedRow_shouldNotSetHighlightedTag() {
189         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
190 
191         mAdapter.updateBackground(mViewHolder, 0);
192 
193         assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
194     }
195 
196     @Test
updateBackground_itemViewIsInvisible_shouldNotSetHighlightedTag()197     public void updateBackground_itemViewIsInvisible_shouldNotSetHighlightedTag() {
198         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
199         ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
200         when(mViewHolder.itemView.isShown()).thenReturn(false);
201 
202         mAdapter.updateBackground(mViewHolder, 0);
203 
204         assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
205     }
206 
207     /**
208      * When background is being updated, we also request the a11y focus on the preference
209      */
210     @Test
updateBackground_shouldRequestAccessibilityFocus()211     public void updateBackground_shouldRequestAccessibilityFocus() {
212         View viewItem = mock(View.class);
213         when(viewItem.isShown()).thenReturn(true);
214         mViewHolder = PreferenceViewHolder.createInstanceForTests(viewItem);
215         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
216 
217         mAdapter.updateBackground(mViewHolder, 10);
218 
219         verify(viewItem).requestAccessibilityFocus();
220     }
221 
222     @Test
updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag()223     public void updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag() {
224         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
225         ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
226         when(mViewHolder.itemView.isShown()).thenReturn(true);
227         assertThat(mAdapter.mFadeInAnimated).isFalse();
228 
229         mAdapter.updateBackground(mViewHolder, 10);
230 
231         assertThat(mAdapter.mFadeInAnimated).isTrue();
232         assertThat(mViewHolder.itemView.getBackground()).isInstanceOf(ColorDrawable.class);
233         assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(true);
234         verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder);
235     }
236 
237     @Test
updateBackground_highlight_itemViewIsInvisible_shouldNotAnimate()238     public void updateBackground_highlight_itemViewIsInvisible_shouldNotAnimate() {
239         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
240         ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
241         when(mViewHolder.itemView.isShown()).thenReturn(false);
242 
243         mAdapter.updateBackground(mViewHolder, 10);
244 
245         assertThat(mAdapter.mFadeInAnimated).isFalse();
246     }
247 
248     @Test
updateBackgroundTwice_highlight_shouldAnimateOnce()249     public void updateBackgroundTwice_highlight_shouldAnimateOnce() {
250         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
251         ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
252         when(mViewHolder.itemView.isShown()).thenReturn(true);
253         assertThat(mAdapter.mFadeInAnimated).isFalse();
254         mAdapter.updateBackground(mViewHolder, 10);
255         // mFadeInAnimated change from false to true - indicating background change is scheduled
256         // through animation.
257         assertThat(mAdapter.mFadeInAnimated).isTrue();
258         // remove highlight should be requested.
259         verify(mAdapter).requestRemoveHighlightDelayed(mViewHolder);
260 
261         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
262         mAdapter.updateBackground(mViewHolder, 10);
263         // only sets background color once - if it's animation this would be called many times
264         verify(mViewHolder.itemView).setBackgroundColor(mAdapter.mHighlightColor);
265         // remove highlight should be requested.
266         verify(mAdapter, times(2)).requestRemoveHighlightDelayed(mViewHolder);
267     }
268 
269     @Test
updateBackground_reuseHighlightedRowForNormalRow_shouldResetBackgroundAndTag()270     public void updateBackground_reuseHighlightedRowForNormalRow_shouldResetBackgroundAndTag() {
271         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
272         mViewHolder.itemView.setTag(R.id.preference_highlighted, true);
273 
274         mAdapter.updateBackground(mViewHolder, 0);
275 
276         assertThat(mViewHolder.itemView.getBackground()).isNotInstanceOf(ColorDrawable.class);
277         assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isEqualTo(false);
278     }
279 }
280