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.test.suitebuilder.annotation.LargeTest; 27import android.util.Log; 28 29import java.util.ArrayList; 30import java.util.List; 31import java.util.Map; 32import java.util.UUID; 33 34import static org.junit.Assert.assertEquals; 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 boolean assignRandomSize() { 157 return false; 158 } 159 }; 160 } 161 waitFirstLayout(); 162 if (mWaitForLayout) { 163 mPostLayoutOperations.run(); 164 } 165 getInstrumentation().waitForIdleSync(); 166 final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt(); 167 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 168 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 169 // we append a suffix to the parcelable to test out of bounds 170 String parcelSuffix = UUID.randomUUID().toString(); 171 Parcel parcel = Parcel.obtain(); 172 savedState.writeToParcel(parcel, 0); 173 parcel.writeString(parcelSuffix); 174 removeRecyclerView(); 175 // reset for reading 176 parcel.setDataPosition(0); 177 // re-create 178 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 179 removeRecyclerView(); 180 181 final int itemCount = mAdapter.getItemCount(); 182 List<Item> mItems = new ArrayList<>(); 183 if (mLoadDataAfterRestore) { 184 mItems.addAll(mAdapter.mItems); 185 mAdapter.deleteAndNotify(0, itemCount); 186 } 187 188 RecyclerView restored = new RecyclerView(getActivity()); 189 mLayoutManager = new WrappedLayoutManager(mConfig.mSpanCount, mConfig.mOrientation); 190 mLayoutManager.setGapStrategy(mConfig.mGapStrategy); 191 restored.setLayoutManager(mLayoutManager); 192 // use the same adapter for Rect matching 193 restored.setAdapter(mAdapter); 194 restored.onRestoreInstanceState(savedState); 195 196 if (mLoadDataAfterRestore) { 197 mAdapter.resetItemsTo(mItems); 198 } 199 200 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 201 parcel.readString()); 202 mLayoutManager.expectLayouts(1); 203 setRecyclerView(restored); 204 mLayoutManager.waitForLayout(2); 205 assertEquals(mConfig + " on saved state, reverse layout should be preserved", 206 mConfig.mReverseLayout, mLayoutManager.getReverseLayout()); 207 assertEquals(mConfig + " on saved state, orientation should be preserved", 208 mConfig.mOrientation, mLayoutManager.getOrientation()); 209 assertEquals(mConfig + " on saved state, span count should be preserved", 210 mConfig.mSpanCount, mLayoutManager.getSpanCount()); 211 assertEquals(mConfig + " on saved state, gap strategy should be preserved", 212 mConfig.mGapStrategy, mLayoutManager.getGapStrategy()); 213 assertEquals(mConfig + " on saved state, first completely visible child position should" 214 + " be preserved", firstCompletelyVisiblePosition, 215 mLayoutManager.findFirstVisibleItemPositionInt()); 216 if (mWaitForLayout) { 217 final boolean strictItemEquality = !mLoadDataAfterRestore; 218 assertRectSetsEqual(mConfig + "\npost layout op:" + mPostLayoutOperations.describe() 219 + ": on restore, previous view positions should be preserved", 220 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 221 } 222 // TODO add tests for changing values after restore before layout 223 } 224 225 static abstract class PostLayoutRunnable { 226 StaggeredGridLayoutManagerSavedStateTest mTest; 227 public void setup(StaggeredGridLayoutManagerSavedStateTest test) { 228 mTest = test; 229 } 230 231 public GridTestAdapter adapter() { 232 return mTest.mAdapter; 233 } 234 235 public WrappedLayoutManager layoutManager() { 236 return mTest.mLayoutManager; 237 } 238 239 public void scrollToPositionWithOffset(int position, int offset) throws Throwable { 240 mTest.scrollToPositionWithOffset(position, offset); 241 } 242 243 public void scrollToPosition(int position) throws Throwable { 244 mTest.scrollToPosition(position); 245 } 246 247 abstract void run() throws Throwable; 248 249 abstract String describe(); 250 251 @Override 252 public String toString() { 253 return describe(); 254 } 255 } 256} 257