1/* 2 * Copyright (C) 2015 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 android.support.v7.widget; 18 19import org.junit.Test; 20import org.junit.runner.RunWith; 21import org.junit.runners.Parameterized; 22 23import android.graphics.Rect; 24import android.os.Parcel; 25import android.os.Parcelable; 26import android.support.test.InstrumentationRegistry; 27import android.test.suitebuilder.annotation.LargeTest; 28import android.util.Log; 29 30import java.util.ArrayList; 31import java.util.List; 32import java.util.Map; 33import java.util.UUID; 34import static org.junit.Assert.*; 35 36@RunWith(Parameterized.class) 37@LargeTest 38public class LinearLayoutManagerSavedStateTest extends BaseLinearLayoutManagerTest { 39 final Config mConfig; 40 final boolean mWaitForLayout; 41 final boolean mLoadDataAfterRestore; 42 final PostLayoutRunnable mPostLayoutOperation; 43 final PostRestoreRunnable mPostRestoreOperation; 44 45 public LinearLayoutManagerSavedStateTest(Config config, boolean waitForLayout, 46 boolean loadDataAfterRestore, PostLayoutRunnable postLayoutOperation, 47 PostRestoreRunnable postRestoreOperation) { 48 mConfig = config; 49 mWaitForLayout = waitForLayout; 50 mLoadDataAfterRestore = loadDataAfterRestore; 51 mPostLayoutOperation = postLayoutOperation; 52 mPostRestoreOperation = postRestoreOperation; 53 mPostLayoutOperation.mLayoutManagerDelegate = new Delegate<WrappedLinearLayoutManager>() { 54 @Override 55 public WrappedLinearLayoutManager get() { 56 return mLayoutManager; 57 } 58 }; 59 mPostLayoutOperation.mTestAdapterDelegate = new Delegate<TestAdapter>() { 60 @Override 61 public TestAdapter get() { 62 return mTestAdapter; 63 } 64 }; 65 mPostRestoreOperation.mLayoutManagerDelegate = new Delegate<WrappedLinearLayoutManager>() { 66 @Override 67 public WrappedLinearLayoutManager get() { 68 return mLayoutManager; 69 } 70 }; 71 mPostRestoreOperation.mTestAdapterDelegate = new Delegate<TestAdapter>() { 72 @Override 73 public TestAdapter get() { 74 return mTestAdapter; 75 } 76 }; 77 } 78 79 @Parameterized.Parameters(name = "{0}_waitForLayout:{1}_loadDataAfterRestore:{2}" 80 + "_postLayout:{3}_postRestore:{4}") 81 public static Iterable<Object[]> params() 82 throws IllegalAccessException, CloneNotSupportedException, NoSuchFieldException { 83 PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ 84 new PostLayoutRunnable() { 85 @Override 86 public void run() throws Throwable { 87 // do nothing 88 } 89 90 @Override 91 public String describe() { 92 return "doing nothing"; 93 } 94 }, 95 new PostLayoutRunnable() { 96 @Override 97 public void run() throws Throwable { 98 layoutManager().expectLayouts(1); 99 scrollToPosition(testAdapter().getItemCount() * 3 / 4); 100 layoutManager().waitForLayout(2); 101 } 102 103 @Override 104 public String describe() { 105 return "scroll to position"; 106 } 107 }, 108 new PostLayoutRunnable() { 109 @Override 110 public void run() throws Throwable { 111 layoutManager().expectLayouts(1); 112 scrollToPositionWithOffset(testAdapter().getItemCount() / 3, 113 50); 114 layoutManager().waitForLayout(2); 115 } 116 117 @Override 118 public String describe() { 119 return "scroll to position with positive offset"; 120 } 121 }, 122 new PostLayoutRunnable() { 123 @Override 124 public void run() throws Throwable { 125 layoutManager().expectLayouts(1); 126 scrollToPositionWithOffset(testAdapter().getItemCount() * 2 / 3, 127 -10); // Some tests break if this value is below the item height. 128 layoutManager().waitForLayout(2); 129 } 130 131 @Override 132 public String describe() { 133 return "scroll to position with negative offset"; 134 } 135 } 136 }; 137 138 PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{ 139 new PostRestoreRunnable() { 140 @Override 141 public String describe() { 142 return "Doing nothing"; 143 } 144 }, 145 new PostRestoreRunnable() { 146 @Override 147 void onAfterRestore(Config config) throws Throwable { 148 // update config as well so that restore assertions will work 149 config.mOrientation = 1 - config.mOrientation; 150 layoutManager().setOrientation(config.mOrientation); 151 } 152 153 @Override 154 boolean shouldLayoutMatch(Config config) { 155 return config.mItemCount == 0; 156 } 157 158 @Override 159 public String describe() { 160 return "Changing orientation"; 161 } 162 }, 163 new PostRestoreRunnable() { 164 @Override 165 void onAfterRestore(Config config) throws Throwable { 166 config.mStackFromEnd = !config.mStackFromEnd; 167 layoutManager().setStackFromEnd(config.mStackFromEnd); 168 } 169 170 @Override 171 boolean shouldLayoutMatch(Config config) { 172 return true; //stack from end should not move items on change 173 } 174 175 @Override 176 public String describe() { 177 return "Changing stack from end"; 178 } 179 }, 180 new PostRestoreRunnable() { 181 @Override 182 void onAfterRestore(Config config) throws Throwable { 183 config.mReverseLayout = !config.mReverseLayout; 184 layoutManager().setReverseLayout(config.mReverseLayout); 185 } 186 187 @Override 188 boolean shouldLayoutMatch(Config config) { 189 return config.mItemCount == 0; 190 } 191 192 @Override 193 public String describe() { 194 return "Changing reverse layout"; 195 } 196 }, 197 new PostRestoreRunnable() { 198 @Override 199 void onAfterRestore(Config config) throws Throwable { 200 config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach; 201 layoutManager().setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 202 } 203 204 @Override 205 boolean shouldLayoutMatch(Config config) { 206 return true; 207 } 208 209 @Override 210 String describe() { 211 return "Change should recycle children"; 212 } 213 }, 214 new PostRestoreRunnable() { 215 int position; 216 @Override 217 void onAfterRestore(Config config) throws Throwable { 218 position = testAdapter().getItemCount() / 2; 219 layoutManager().scrollToPosition(position); 220 } 221 222 @Override 223 boolean shouldLayoutMatch(Config config) { 224 return testAdapter().getItemCount() == 0; 225 } 226 227 @Override 228 String describe() { 229 return "Scroll to position " + position ; 230 } 231 232 @Override 233 void onAfterReLayout(Config config) { 234 if (testAdapter().getItemCount() > 0) { 235 assertEquals(config + ":scrolled view should be last completely visible", 236 position, 237 config.mStackFromEnd ? 238 layoutManager().findLastCompletelyVisibleItemPosition() 239 : layoutManager().findFirstCompletelyVisibleItemPosition()); 240 } 241 } 242 } 243 }; 244 boolean[] waitForLayoutOptions = new boolean[]{true, false}; 245 boolean[] loadDataAfterRestoreOptions = new boolean[]{true, false}; 246 List<Config> variations = addConfigVariation(createBaseVariations(), "mItemCount", 0, 300); 247 variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true); 248 249 List<Object[]> params = new ArrayList<>(); 250 for (Config config : variations) { 251 for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) { 252 for (boolean waitForLayout : waitForLayoutOptions) { 253 for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) { 254 for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) { 255 params.add(new Object[]{ 256 config.clone(), waitForLayout, 257 loadDataAfterRestore, postLayoutRunnable, postRestoreRunnable 258 }); 259 } 260 } 261 262 } 263 } 264 } 265 return params; 266 } 267 268 @Test 269 public void savedStateTest() 270 throws Throwable { 271 if (DEBUG) { 272 Log.d(TAG, "testing saved state with wait for layout = " + mWaitForLayout + " config " + 273 mConfig + " post layout action " + mPostLayoutOperation.describe() + 274 "post restore action " + mPostRestoreOperation.describe()); 275 } 276 setupByConfig(mConfig, false); 277 278 if (mWaitForLayout) { 279 waitForFirstLayout(); 280 mPostLayoutOperation.run(); 281 } 282 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 283 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 284 // we append a suffix to the parcelable to test out of bounds 285 String parcelSuffix = UUID.randomUUID().toString(); 286 Parcel parcel = Parcel.obtain(); 287 savedState.writeToParcel(parcel, 0); 288 parcel.writeString(parcelSuffix); 289 removeRecyclerView(); 290 // reset for reading 291 parcel.setDataPosition(0); 292 // re-create 293 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 294 295 final int itemCount = mTestAdapter.getItemCount(); 296 List<Item> testItems = new ArrayList<>(); 297 if (mLoadDataAfterRestore) { 298 // we cannot delete and re-add since new items may have different sizes. We need the 299 // exact same adapter. 300 testItems.addAll(mTestAdapter.mItems); 301 mTestAdapter.deleteAndNotify(0, itemCount); 302 } 303 304 RecyclerView restored = new RecyclerView(getActivity()); 305 // this config should be no op. 306 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), 307 mConfig.mOrientation, mConfig.mReverseLayout); 308 mLayoutManager.setStackFromEnd(mConfig.mStackFromEnd); 309 restored.setLayoutManager(mLayoutManager); 310 // use the same adapter for Rect matching 311 restored.setAdapter(mTestAdapter); 312 restored.onRestoreInstanceState(savedState); 313 314 if (mLoadDataAfterRestore) { 315 // add the same items back 316 mTestAdapter.resetItemsTo(testItems); 317 } 318 319 mPostRestoreOperation.onAfterRestore(mConfig); 320 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 321 parcel.readString()); 322 mLayoutManager.expectLayouts(1); 323 setRecyclerView(restored); 324 mLayoutManager.waitForLayout(2); 325 // calculate prefix here instead of above to include post restore changes 326 final String logPrefix = mConfig + "\npostLayout:" + mPostLayoutOperation.describe() + 327 "\npostRestore:" + mPostRestoreOperation.describe() + "\n"; 328 assertEquals(logPrefix + " on saved state, reverse layout should be preserved", 329 mConfig.mReverseLayout, mLayoutManager.getReverseLayout()); 330 assertEquals(logPrefix + " on saved state, orientation should be preserved", 331 mConfig.mOrientation, mLayoutManager.getOrientation()); 332 assertEquals(logPrefix + " on saved state, stack from end should be preserved", 333 mConfig.mStackFromEnd, mLayoutManager.getStackFromEnd()); 334 if (mWaitForLayout) { 335 final boolean strictItemEquality = !mLoadDataAfterRestore; 336 if (mPostRestoreOperation.shouldLayoutMatch(mConfig)) { 337 assertRectSetsEqual( 338 logPrefix + ": on restore, previous view positions should be preserved", 339 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 340 } else { 341 assertRectSetsNotEqual( 342 logPrefix 343 + ": on restore with changes, previous view positions should NOT " 344 + "be preserved", 345 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 346 } 347 mPostRestoreOperation.onAfterReLayout(mConfig); 348 } 349 } 350 351 protected static abstract class PostLayoutRunnable { 352 private Delegate<WrappedLinearLayoutManager> mLayoutManagerDelegate; 353 private Delegate<TestAdapter> mTestAdapterDelegate; 354 protected WrappedLinearLayoutManager layoutManager() { 355 return mLayoutManagerDelegate.get(); 356 } 357 protected TestAdapter testAdapter() { 358 return mTestAdapterDelegate.get(); 359 } 360 361 abstract void run() throws Throwable; 362 void scrollToPosition(final int position) { 363 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 364 @Override 365 public void run() { 366 layoutManager().scrollToPosition(position); 367 } 368 }); 369 } 370 void scrollToPositionWithOffset(final int position, final int offset) { 371 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 372 @Override 373 public void run() { 374 layoutManager().scrollToPositionWithOffset(position, offset); 375 } 376 }); 377 } 378 abstract String describe(); 379 380 @Override 381 public String toString() { 382 return describe(); 383 } 384 } 385 386 protected static abstract class PostRestoreRunnable { 387 private Delegate<WrappedLinearLayoutManager> mLayoutManagerDelegate; 388 private Delegate<TestAdapter> mTestAdapterDelegate; 389 protected WrappedLinearLayoutManager layoutManager() { 390 return mLayoutManagerDelegate.get(); 391 } 392 protected TestAdapter testAdapter() { 393 return mTestAdapterDelegate.get(); 394 } 395 396 void onAfterRestore(Config config) throws Throwable { 397 } 398 399 abstract String describe(); 400 401 boolean shouldLayoutMatch(Config config) { 402 return true; 403 } 404 405 void onAfterReLayout(Config config) { 406 407 }; 408 409 @Override 410 public String toString() { 411 return describe(); 412 } 413 } 414 415 private interface Delegate<T> { 416 T get(); 417 } 418} 419