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.junit.Assert.assertEquals; 20import static org.junit.Assert.assertFalse; 21import static org.junit.Assert.assertNotNull; 22import static org.junit.Assert.assertTrue; 23 24import android.os.Build; 25import android.support.test.filters.MediumTest; 26import android.view.View; 27import android.view.accessibility.AccessibilityEvent; 28 29import androidx.core.view.AccessibilityDelegateCompat; 30import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 31 32import org.junit.Test; 33import org.junit.runner.RunWith; 34import org.junit.runners.Parameterized; 35 36import java.util.ArrayList; 37import java.util.List; 38import java.util.concurrent.atomic.AtomicBoolean; 39 40@MediumTest 41@RunWith(Parameterized.class) 42public class RecyclerViewAccessibilityTest extends BaseRecyclerViewInstrumentationTest { 43 private static final boolean SUPPORTS_COLLECTION_INFO = 44 Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 45 private final boolean mVerticalScrollBefore; 46 private final boolean mHorizontalScrollBefore; 47 private final boolean mVerticalScrollAfter; 48 private final boolean mHorizontalScrollAfter; 49 50 public RecyclerViewAccessibilityTest(boolean verticalScrollBefore, 51 boolean horizontalScrollBefore, boolean verticalScrollAfter, 52 boolean horizontalScrollAfter) { 53 mVerticalScrollBefore = verticalScrollBefore; 54 mHorizontalScrollBefore = horizontalScrollBefore; 55 mVerticalScrollAfter = verticalScrollAfter; 56 mHorizontalScrollAfter = horizontalScrollAfter; 57 } 58 59 @Parameterized.Parameters(name = "vBefore={0},vAfter={1},hBefore={2},hAfter={3}") 60 public static List<Object[]> getParams() { 61 List<Object[]> params = new ArrayList<>(); 62 for (boolean vBefore : new boolean[]{true, false}) { 63 for (boolean vAfter : new boolean[]{true, false}) { 64 for (boolean hBefore : new boolean[]{true, false}) { 65 for (boolean hAfter : new boolean[]{true, false}) { 66 params.add(new Object[]{vBefore, hBefore, vAfter, hAfter}); 67 } 68 } 69 } 70 } 71 return params; 72 } 73 74 @Test 75 public void onInitializeAccessibilityNodeInfoTest() throws Throwable { 76 final RecyclerView recyclerView = new RecyclerView(getActivity()) { 77 @Override 78 public boolean canScrollHorizontally(int direction) { 79 return direction < 0 && mHorizontalScrollBefore || 80 direction > 0 && mHorizontalScrollAfter; 81 } 82 83 @Override 84 public boolean canScrollVertically(int direction) { 85 return direction < 0 && mVerticalScrollBefore || 86 direction > 0 && mVerticalScrollAfter; 87 } 88 }; 89 final TestAdapter adapter = new TestAdapter(10); 90 final AtomicBoolean hScrolledBack = new AtomicBoolean(false); 91 final AtomicBoolean vScrolledBack = new AtomicBoolean(false); 92 final AtomicBoolean hScrolledFwd = new AtomicBoolean(false); 93 final AtomicBoolean vScrolledFwd = new AtomicBoolean(false); 94 recyclerView.setAdapter(adapter); 95 recyclerView.setLayoutManager(new TestLayoutManager() { 96 97 @Override 98 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 99 layoutRange(recycler, 0, 5); 100 } 101 102 @Override 103 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 104 return new RecyclerView.LayoutParams(-1, -1); 105 } 106 107 @Override 108 public boolean canScrollVertically() { 109 return mVerticalScrollAfter || mVerticalScrollBefore; 110 } 111 112 @Override 113 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 114 RecyclerView.State state) { 115 if (dx > 0) { 116 hScrolledFwd.set(true); 117 } else if (dx < 0) { 118 hScrolledBack.set(true); 119 } 120 return 0; 121 } 122 123 @Override 124 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 125 RecyclerView.State state) { 126 if (dy > 0) { 127 vScrolledFwd.set(true); 128 } else if (dy < 0) { 129 vScrolledBack.set(true); 130 } 131 return 0; 132 } 133 134 @Override 135 public boolean canScrollHorizontally() { 136 return mHorizontalScrollAfter || mHorizontalScrollBefore; 137 } 138 }); 139 setRecyclerView(recyclerView); 140 final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView 141 .getCompatAccessibilityDelegate(); 142 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 143 mActivityRule.runOnUiThread(new Runnable() { 144 @Override 145 public void run() { 146 delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info); 147 } 148 }); 149 assertEquals(mHorizontalScrollAfter || mHorizontalScrollBefore 150 || mVerticalScrollAfter || mVerticalScrollBefore, info.isScrollable()); 151 assertEquals(mHorizontalScrollBefore || mVerticalScrollBefore, 152 (info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD) != 0); 153 assertEquals(mHorizontalScrollAfter || mVerticalScrollAfter, 154 (info.getActions() & AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD) != 0); 155 if (SUPPORTS_COLLECTION_INFO) { 156 final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = info 157 .getCollectionInfo(); 158 assertNotNull(collectionInfo); 159 if (recyclerView.getLayoutManager().canScrollVertically()) { 160 assertEquals(adapter.getItemCount(), collectionInfo.getRowCount()); 161 } 162 if (recyclerView.getLayoutManager().canScrollHorizontally()) { 163 assertEquals(adapter.getItemCount(), collectionInfo.getColumnCount()); 164 } 165 } 166 167 final AccessibilityEvent event = AccessibilityEvent.obtain(); 168 mActivityRule.runOnUiThread(new Runnable() { 169 @Override 170 public void run() { 171 delegateCompat.onInitializeAccessibilityEvent(recyclerView, event); 172 } 173 }); 174 assertEquals(event.isScrollable(), mVerticalScrollAfter || mHorizontalScrollAfter 175 || mVerticalScrollBefore || mHorizontalScrollBefore); 176 assertEquals(event.getItemCount(), adapter.getItemCount()); 177 178 getInstrumentation().waitForIdleSync(); 179 if (SUPPORTS_COLLECTION_INFO) { 180 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 181 final View view = mRecyclerView.getChildAt(i); 182 final AccessibilityNodeInfoCompat childInfo = AccessibilityNodeInfoCompat.obtain(); 183 mActivityRule.runOnUiThread(new Runnable() { 184 @Override 185 public void run() { 186 delegateCompat.getItemDelegate(). 187 onInitializeAccessibilityNodeInfo(view, childInfo); 188 } 189 }); 190 final AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfo 191 = childInfo.getCollectionItemInfo(); 192 assertNotNull(collectionItemInfo); 193 if (recyclerView.getLayoutManager().canScrollHorizontally()) { 194 assertEquals(i, collectionItemInfo.getColumnIndex()); 195 } else { 196 assertEquals(0, collectionItemInfo.getColumnIndex()); 197 } 198 199 if (recyclerView.getLayoutManager().canScrollVertically()) { 200 assertEquals(i, collectionItemInfo.getRowIndex()); 201 } else { 202 assertEquals(0, collectionItemInfo.getRowIndex()); 203 } 204 } 205 } 206 207 mActivityRule.runOnUiThread(new Runnable() { 208 @Override 209 public void run() { 210 211 } 212 }); 213 hScrolledBack.set(false); 214 vScrolledBack.set(false); 215 hScrolledFwd.set(false); 216 vScrolledBack.set(false); 217 performAccessibilityAction(delegateCompat, recyclerView, 218 AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 219 assertEquals(mHorizontalScrollBefore, hScrolledBack.get()); 220 assertEquals(mVerticalScrollBefore, vScrolledBack.get()); 221 assertEquals(false, hScrolledFwd.get()); 222 assertEquals(false, vScrolledFwd.get()); 223 224 hScrolledBack.set(false); 225 vScrolledBack.set(false); 226 hScrolledFwd.set(false); 227 vScrolledBack.set(false); 228 performAccessibilityAction(delegateCompat, recyclerView, 229 AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 230 assertEquals(false, hScrolledBack.get()); 231 assertEquals(false, vScrolledBack.get()); 232 assertEquals(mHorizontalScrollAfter, hScrolledFwd.get()); 233 assertEquals(mVerticalScrollAfter, vScrolledFwd.get()); 234 } 235 236 @Test 237 public void ignoreAccessibilityIfAdapterHasChanged() throws Throwable { 238 final RecyclerView recyclerView = new RecyclerView(getActivity()) { 239 //@Override 240 @Override 241 public boolean canScrollHorizontally(int direction) { 242 return true; 243 } 244 245 //@Override 246 @Override 247 public boolean canScrollVertically(int direction) { 248 return true; 249 } 250 }; 251 final DumbLayoutManager layoutManager = new DumbLayoutManager(); 252 final TestAdapter adapter = new TestAdapter(10); 253 recyclerView.setAdapter(adapter); 254 recyclerView.setLayoutManager(layoutManager); 255 layoutManager.expectLayouts(1); 256 setRecyclerView(recyclerView); 257 layoutManager.waitForLayout(1); 258 259 final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView 260 .getCompatAccessibilityDelegate(); 261 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 262 mActivityRule.runOnUiThread(new Runnable() { 263 @Override 264 public void run() { 265 delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info); 266 } 267 }); 268 assertTrue("test sanity", info.isScrollable()); 269 final AccessibilityNodeInfoCompat info2 = AccessibilityNodeInfoCompat.obtain(); 270 mActivityRule.runOnUiThread(new Runnable() { 271 @Override 272 public void run() { 273 try { 274 adapter.deleteAndNotify(1, 1); 275 } catch (Throwable throwable) { 276 postExceptionToInstrumentation(throwable); 277 } 278 delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info2); 279 assertFalse("info should not be filled if data is out of date", 280 info2.isScrollable()); 281 } 282 }); 283 checkForMainThreadException(); 284 } 285 286 boolean performAccessibilityAction(final AccessibilityDelegateCompat delegate, 287 final RecyclerView recyclerView, final int action) throws Throwable { 288 final boolean[] result = new boolean[1]; 289 mActivityRule.runOnUiThread(new Runnable() { 290 @Override 291 public void run() { 292 result[0] = delegate.performAccessibilityAction(recyclerView, action, null); 293 } 294 }); 295 getInstrumentation().waitForIdleSync(); 296 Thread.sleep(250); 297 return result[0]; 298 } 299} 300