1/* 2 * Copyright 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 17package androidx.recyclerview.widget; 18 19import static org.hamcrest.CoreMatchers.is; 20import static org.hamcrest.MatcherAssert.assertThat; 21import static org.junit.Assert.assertEquals; 22import static org.junit.Assert.assertFalse; 23import static org.junit.Assert.assertNotNull; 24import static org.junit.Assert.assertTrue; 25import static org.mockito.Matchers.anyInt; 26import static org.mockito.Mockito.never; 27import static org.mockito.Mockito.spy; 28import static org.mockito.Mockito.verify; 29 30import android.os.Build; 31import android.support.test.filters.LargeTest; 32import android.support.test.filters.MediumTest; 33import android.support.test.filters.SdkSuppress; 34import android.support.test.runner.AndroidJUnit4; 35import android.view.View; 36import android.view.ViewGroup; 37import android.view.accessibility.AccessibilityNodeInfo; 38 39import androidx.annotation.NonNull; 40import androidx.core.view.AccessibilityDelegateCompat; 41import androidx.core.view.ViewCompat; 42import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 43 44import org.junit.Test; 45import org.junit.runner.RunWith; 46 47import java.util.ArrayList; 48import java.util.Arrays; 49import java.util.List; 50import java.util.concurrent.atomic.AtomicBoolean; 51 52@MediumTest 53@RunWith(AndroidJUnit4.class) 54public class RecyclerViewAccessibilityLifecycleTest extends BaseRecyclerViewInstrumentationTest { 55 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) 56 @Test 57 public void dontDispatchChangeDuringLayout() throws Throwable { 58 LayoutAllLayoutManager lm = new LayoutAllLayoutManager(); 59 final AtomicBoolean calledA11DuringLayout = new AtomicBoolean(false); 60 final List<Integer> invocations = new ArrayList<>(); 61 62 final WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity()) { 63 @Override 64 boolean isAccessibilityEnabled() { 65 return true; 66 } 67 68 @Override 69 public boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, 70 int importantForAccessibilityBeforeHidden) { 71 invocations.add(importantForAccessibilityBeforeHidden); 72 boolean notified = super.setChildImportantForAccessibilityInternal(viewHolder, 73 importantForAccessibilityBeforeHidden); 74 if (notified && mRecyclerView.isComputingLayout()) { 75 calledA11DuringLayout.set(true); 76 } 77 return notified; 78 } 79 }; 80 TestAdapter adapter = new TestAdapter(10) { 81 @Override 82 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 83 int viewType) { 84 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 85 ViewCompat.setImportantForAccessibility(vh.itemView, 86 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 87 return vh; 88 } 89 }; 90 recyclerView.setAdapter(adapter); 91 recyclerView.setLayoutManager(lm); 92 lm.expectLayouts(1); 93 setRecyclerView(recyclerView); 94 lm.waitForLayout(1); 95 assertThat(calledA11DuringLayout.get(), is(false)); 96 lm.expectLayouts(1); 97 adapter.deleteAndNotify(2, 2); 98 lm.waitForLayout(2); 99 recyclerView.waitUntilAnimations(); 100 assertThat(invocations, is(Arrays.asList( 101 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, 102 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, 103 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES, 104 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES))); 105 106 assertThat(calledA11DuringLayout.get(), is(false)); 107 } 108 109 @LargeTest 110 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) 111 @Test 112 public void processAllViewHolders() { 113 RecyclerView rv = new RecyclerView(getActivity()); 114 rv.setLayoutManager(new LinearLayoutManager(getActivity())); 115 View itemView1 = spy(new View(getActivity())); 116 View itemView2 = spy(new View(getActivity())); 117 View itemView3 = spy(new View(getActivity())); 118 119 rv.addView(itemView1); 120 // do not add 2 121 rv.addView(itemView3); 122 123 RecyclerView.ViewHolder vh1 = new RecyclerView.ViewHolder(itemView1) {}; 124 vh1.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_YES; 125 RecyclerView.ViewHolder vh2 = new RecyclerView.ViewHolder(itemView2) {}; 126 vh2.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_YES; 127 RecyclerView.ViewHolder vh3 = new RecyclerView.ViewHolder(itemView3) {}; 128 vh3.mPendingAccessibilityState = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; 129 130 rv.mPendingAccessibilityImportanceChange.add(vh1); 131 rv.mPendingAccessibilityImportanceChange.add(vh2); 132 rv.mPendingAccessibilityImportanceChange.add(vh3); 133 rv.dispatchPendingImportantForAccessibilityChanges(); 134 135 verify(itemView1).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 136 //noinspection WrongConstant 137 verify(itemView2, never()).setImportantForAccessibility(anyInt()); 138 verify(itemView3).setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); 139 assertThat(rv.mPendingAccessibilityImportanceChange.size(), is(0)); 140 } 141 142 public class LayoutAllLayoutManager extends TestLayoutManager { 143 private final boolean mAllowNullLayoutLatch; 144 145 public LayoutAllLayoutManager() { 146 // by default, we don't allow unexpected layouts. 147 this(false); 148 } 149 LayoutAllLayoutManager(boolean allowNullLayoutLatch) { 150 mAllowNullLayoutLatch = allowNullLayoutLatch; 151 } 152 153 @Override 154 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 155 detachAndScrapAttachedViews(recycler); 156 layoutRange(recycler, 0, state.getItemCount()); 157 if (!mAllowNullLayoutLatch || layoutLatch != null) { 158 layoutLatch.countDown(); 159 } 160 } 161 } 162 163 @Test 164 public void notClearCustomViewDelegate() throws Throwable { 165 final RecyclerView recyclerView = new RecyclerView(getActivity()) { 166 @Override 167 boolean isAccessibilityEnabled() { 168 return true; 169 } 170 }; 171 final int[] layoutStart = new int[] {0}; 172 final int layoutCount = 5; 173 final TestLayoutManager layoutManager = new TestLayoutManager() { 174 @Override 175 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 176 detachAndScrapAttachedViews(recycler); 177 removeAndRecycleScrapInt(recycler); 178 layoutRange(recycler, layoutStart[0], layoutStart[0] + layoutCount); 179 if (layoutLatch != null) { 180 layoutLatch.countDown(); 181 } 182 } 183 }; 184 final AccessibilityDelegateCompat delegateCompat = new AccessibilityDelegateCompat() { 185 @Override 186 public void onInitializeAccessibilityNodeInfo(View host, 187 AccessibilityNodeInfoCompat info) { 188 super.onInitializeAccessibilityNodeInfo(host, info); 189 info.setChecked(true); 190 } 191 }; 192 final TestAdapter adapter = new TestAdapter(100) { 193 @Override 194 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 195 int viewType) { 196 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 197 ViewCompat.setAccessibilityDelegate(vh.itemView, delegateCompat); 198 return vh; 199 } 200 }; 201 layoutManager.expectLayouts(1); 202 recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100); 203 recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool 204 recyclerView.setLayoutManager(layoutManager); 205 setRecyclerView(recyclerView); 206 mActivityRule.runOnUiThread(new Runnable() { 207 @Override 208 public void run() { 209 recyclerView.setAdapter(adapter); 210 } 211 }); 212 layoutManager.waitForLayout(1); 213 214 assertEquals(layoutCount, recyclerView.getChildCount()); 215 final ArrayList<View> children = new ArrayList(); 216 mActivityRule.runOnUiThread(new Runnable() { 217 @Override 218 public void run() { 219 for (int i = 0; i < recyclerView.getChildCount(); i++) { 220 View view = recyclerView.getChildAt(i); 221 assertEquals(layoutStart[0] + i, 222 recyclerView.getChildAdapterPosition(view)); 223 AccessibilityNodeInfo info = recyclerView.getChildAt(i) 224 .createAccessibilityNodeInfo(); 225 assertTrue("custom delegate sets isChecked", info.isChecked()); 226 assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags( 227 RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)); 228 assertTrue(ViewCompat.hasAccessibilityDelegate(view)); 229 children.add(view); 230 } 231 } 232 }); 233 234 // invalidate and start layout at 50, all existing views will goes to recycler and 235 // being reused. 236 layoutStart[0] = 50; 237 layoutManager.expectLayouts(1); 238 adapter.dispatchDataSetChanged(); 239 layoutManager.waitForLayout(1); 240 assertEquals(layoutCount, recyclerView.getChildCount()); 241 mActivityRule.runOnUiThread(new Runnable() { 242 @Override 243 public void run() { 244 for (int i = 0; i < recyclerView.getChildCount(); i++) { 245 View view = recyclerView.getChildAt(i); 246 assertEquals(layoutStart[0] + i, 247 recyclerView.getChildAdapterPosition(view)); 248 assertTrue(children.contains(view)); 249 AccessibilityNodeInfo info = view.createAccessibilityNodeInfo(); 250 assertTrue("custom delegate sets isChecked", info.isChecked()); 251 assertFalse(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags( 252 RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)); 253 assertTrue(ViewCompat.hasAccessibilityDelegate(view)); 254 } 255 } 256 }); 257 } 258 259 @Test 260 public void clearItemDelegateWhenGoesToPool() throws Throwable { 261 final RecyclerView recyclerView = new RecyclerView(getActivity()) { 262 @Override 263 boolean isAccessibilityEnabled() { 264 return true; 265 } 266 }; 267 final int firstPassLayoutCount = 5; 268 final int[] layoutCount = new int[] {firstPassLayoutCount}; 269 final TestLayoutManager layoutManager = new TestLayoutManager() { 270 @Override 271 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 272 detachAndScrapAttachedViews(recycler); 273 removeAndRecycleScrapInt(recycler); 274 layoutRange(recycler, 0, layoutCount[0]); 275 if (layoutLatch != null) { 276 layoutLatch.countDown(); 277 } 278 } 279 }; 280 final TestAdapter adapter = new TestAdapter(100); 281 layoutManager.expectLayouts(1); 282 recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 100); 283 recyclerView.setItemViewCacheSize(0); // no cache, directly goes to pool 284 recyclerView.setLayoutManager(layoutManager); 285 setRecyclerView(recyclerView); 286 mActivityRule.runOnUiThread(new Runnable() { 287 @Override 288 public void run() { 289 recyclerView.setAdapter(adapter); 290 } 291 }); 292 layoutManager.waitForLayout(1); 293 294 assertEquals(firstPassLayoutCount, recyclerView.getChildCount()); 295 mActivityRule.runOnUiThread(new Runnable() { 296 @Override 297 public void run() { 298 for (int i = 0; i < recyclerView.getChildCount(); i++) { 299 View view = recyclerView.getChildAt(i); 300 assertEquals(i, recyclerView.getChildAdapterPosition(view)); 301 assertTrue(recyclerView.findContainingViewHolder(view).hasAnyOfTheFlags( 302 RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)); 303 assertTrue(ViewCompat.hasAccessibilityDelegate(view)); 304 AccessibilityNodeInfo info = view.createAccessibilityNodeInfo(); 305 if (Build.VERSION.SDK_INT >= 19) { 306 assertNotNull(info.getCollectionItemInfo()); 307 } 308 } 309 } 310 }); 311 312 // let all items go to recycler pool 313 layoutManager.expectLayouts(1); 314 layoutCount[0] = 0; 315 adapter.resetItemsTo(new ArrayList()); 316 layoutManager.waitForLayout(1); 317 assertEquals(0, recyclerView.getChildCount()); 318 assertEquals(firstPassLayoutCount, recyclerView.getRecycledViewPool() 319 .getRecycledViewCount(0)); 320 mActivityRule.runOnUiThread(new Runnable() { 321 @Override 322 public void run() { 323 for (int i = 0; i < firstPassLayoutCount; i++) { 324 RecyclerView.ViewHolder vh = recyclerView.getRecycledViewPool() 325 .getRecycledView(0); 326 View view = vh.itemView; 327 assertEquals(RecyclerView.NO_POSITION, 328 recyclerView.getChildAdapterPosition(view)); 329 assertFalse(vh.hasAnyOfTheFlags( 330 RecyclerView.ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)); 331 assertFalse(ViewCompat.hasAccessibilityDelegate(view)); 332 } 333 } 334 }); 335 336 } 337} 338