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; 20 21import android.graphics.Rect; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.support.test.filters.LargeTest; 25import android.util.Log; 26 27import org.junit.Test; 28import org.junit.runner.RunWith; 29import org.junit.runners.Parameterized; 30 31import java.util.ArrayList; 32import java.util.List; 33import java.util.Map; 34import java.util.UUID; 35 36@RunWith(Parameterized.class) 37@LargeTest 38public class StaggeredGridLayoutManagerSavedStateTest extends BaseStaggeredGridLayoutManagerTest { 39 private final Config mConfig; 40 private final boolean mWaitForLayout; 41 private final boolean mLoadDataAfterRestore; 42 private final PostLayoutRunnable mPostLayoutOperations; 43 44 public StaggeredGridLayoutManagerSavedStateTest( 45 Config config, boolean waitForLayout, boolean loadDataAfterRestore, 46 PostLayoutRunnable postLayoutOperations) throws CloneNotSupportedException { 47 this.mConfig = (Config) config.clone(); 48 this.mWaitForLayout = waitForLayout; 49 this.mLoadDataAfterRestore = loadDataAfterRestore; 50 this.mPostLayoutOperations = postLayoutOperations; 51 if (postLayoutOperations != null) { 52 postLayoutOperations.mTest = this; 53 } 54 } 55 56 @Parameterized.Parameters(name = "config={0},waitForLayout={1},loadDataAfterRestore={2}" 57 + ",postLayoutRunnable={3}") 58 public static List<Object[]> getParams() throws CloneNotSupportedException { 59 List<Config> variations = createBaseVariations(); 60 61 PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ 62 new PostLayoutRunnable() { 63 @Override 64 public void run() throws Throwable { 65 // do nothing 66 } 67 68 @Override 69 public String describe() { 70 return "doing_nothing"; 71 } 72 }, 73 new PostLayoutRunnable() { 74 @Override 75 public void run() throws Throwable { 76 layoutManager().expectLayouts(1); 77 scrollToPosition(adapter().getItemCount() * 3 / 4); 78 layoutManager().waitForLayout(2); 79 } 80 81 @Override 82 public String describe() { 83 return "scroll_to_position_item_count*3/4"; 84 } 85 }, 86 new PostLayoutRunnable() { 87 @Override 88 public void run() throws Throwable { 89 layoutManager().expectLayouts(1); 90 scrollToPositionWithOffset(adapter().getItemCount() / 3, 91 50); 92 layoutManager().waitForLayout(2); 93 } 94 95 @Override 96 public String describe() { 97 return "scroll_to_position_item_count/3_with_positive_offset"; 98 } 99 }, 100 new PostLayoutRunnable() { 101 @Override 102 public void run() throws Throwable { 103 layoutManager().expectLayouts(1); 104 scrollToPositionWithOffset(adapter().getItemCount() * 2 / 3, 105 -50); 106 layoutManager().waitForLayout(2); 107 } 108 109 @Override 110 public String describe() { 111 return "scroll_to_position_with_negative_offset"; 112 } 113 } 114 }; 115 boolean[] waitForLayoutOptions = new boolean[]{false, true}; 116 boolean[] loadDataAfterRestoreOptions = new boolean[]{false, true}; 117 List<Config> testVariations = new ArrayList<Config>(); 118 testVariations.addAll(variations); 119 for (Config config : variations) { 120 if (config.mSpanCount < 2) { 121 continue; 122 } 123 final Config clone = (Config) config.clone(); 124 clone.mItemCount = clone.mSpanCount - 1; 125 testVariations.add(clone); 126 } 127 128 List<Object[]> params = new ArrayList<>(); 129 for (Config config : testVariations) { 130 for (PostLayoutRunnable runnable : postLayoutOptions) { 131 for (boolean waitForLayout : waitForLayoutOptions) { 132 for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) { 133 params.add(new Object[]{config, waitForLayout, loadDataAfterRestore, 134 runnable}); 135 } 136 } 137 } 138 } 139 return params; 140 } 141 142 @Test 143 public void savedState() throws Throwable { 144 if (DEBUG) { 145 Log.d(TAG, "testing saved state with wait for layout = " + mWaitForLayout + " config " 146 + mConfig + " post layout action " + mPostLayoutOperations.describe()); 147 } 148 setupByConfig(mConfig); 149 if (mLoadDataAfterRestore) { 150 // We are going to re-create items, force non-random item size. 151 mAdapter.mOnBindCallback = new OnBindCallback() { 152 @Override 153 void onBoundItem(TestViewHolder vh, int position) { 154 } 155 156 @Override 157 boolean assignRandomSize() { 158 return false; 159 } 160 }; 161 } 162 waitFirstLayout(); 163 if (mWaitForLayout) { 164 mPostLayoutOperations.run(); 165 } 166 getInstrumentation().waitForIdleSync(); 167 final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt(); 168 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 169 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 170 // we append a suffix to the parcelable to test out of bounds 171 String parcelSuffix = UUID.randomUUID().toString(); 172 Parcel parcel = Parcel.obtain(); 173 savedState.writeToParcel(parcel, 0); 174 parcel.writeString(parcelSuffix); 175 removeRecyclerView(); 176 // reset for reading 177 parcel.setDataPosition(0); 178 // re-create 179 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 180 removeRecyclerView(); 181 182 final int itemCount = mAdapter.getItemCount(); 183 List<Item> mItems = new ArrayList<>(); 184 if (mLoadDataAfterRestore) { 185 mItems.addAll(mAdapter.mItems); 186 mAdapter.deleteAndNotify(0, itemCount); 187 } 188 189 RecyclerView restored = new RecyclerView(getActivity()); 190 mLayoutManager = new WrappedLayoutManager(mConfig.mSpanCount, mConfig.mOrientation); 191 mLayoutManager.setGapStrategy(mConfig.mGapStrategy); 192 restored.setLayoutManager(mLayoutManager); 193 // use the same adapter for Rect matching 194 restored.setAdapter(mAdapter); 195 restored.onRestoreInstanceState(savedState); 196 197 if (mLoadDataAfterRestore) { 198 mAdapter.resetItemsTo(mItems); 199 } 200 201 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 202 parcel.readString()); 203 mLayoutManager.expectLayouts(1); 204 setRecyclerView(restored); 205 mLayoutManager.waitForLayout(2); 206 assertEquals(mConfig + " on saved state, reverse layout should be preserved", 207 mConfig.mReverseLayout, mLayoutManager.getReverseLayout()); 208 assertEquals(mConfig + " on saved state, orientation should be preserved", 209 mConfig.mOrientation, mLayoutManager.getOrientation()); 210 assertEquals(mConfig + " on saved state, span count should be preserved", 211 mConfig.mSpanCount, mLayoutManager.getSpanCount()); 212 assertEquals(mConfig + " on saved state, gap strategy should be preserved", 213 mConfig.mGapStrategy, mLayoutManager.getGapStrategy()); 214 assertEquals(mConfig + " on saved state, first completely visible child position should" 215 + " be preserved", firstCompletelyVisiblePosition, 216 mLayoutManager.findFirstVisibleItemPositionInt()); 217 if (mWaitForLayout) { 218 final boolean strictItemEquality = !mLoadDataAfterRestore; 219 assertRectSetsEqual(mConfig + "\npost layout op:" + mPostLayoutOperations.describe() 220 + ": on restore, previous view positions should be preserved", 221 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 222 } 223 // TODO add tests for changing values after restore before layout 224 } 225 226 static abstract class PostLayoutRunnable { 227 StaggeredGridLayoutManagerSavedStateTest mTest; 228 public void setup(StaggeredGridLayoutManagerSavedStateTest test) { 229 mTest = test; 230 } 231 232 public GridTestAdapter adapter() { 233 return mTest.mAdapter; 234 } 235 236 public WrappedLayoutManager layoutManager() { 237 return mTest.mLayoutManager; 238 } 239 240 public void scrollToPositionWithOffset(int position, int offset) throws Throwable { 241 mTest.scrollToPositionWithOffset(position, offset); 242 } 243 244 public void scrollToPosition(int position) throws Throwable { 245 mTest.scrollToPosition(position); 246 } 247 248 abstract void run() throws Throwable; 249 250 abstract String describe(); 251 252 @Override 253 public String toString() { 254 return describe(); 255 } 256 } 257} 258