1 /* 2 * Copyright 2017 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 17 package androidx.car.widget; 18 19 import static android.support.test.espresso.Espresso.onView; 20 import static android.support.test.espresso.action.ViewActions.click; 21 import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; 22 import static android.support.test.espresso.matcher.ViewMatchers.withId; 23 24 import static org.hamcrest.Matchers.allOf; 25 import static org.junit.Assert.assertEquals; 26 27 import android.content.pm.ActivityInfo; 28 import android.content.pm.PackageManager; 29 import android.support.test.espresso.IdlingRegistry; 30 import android.support.test.espresso.IdlingResource; 31 import android.support.test.filters.SmallTest; 32 import android.support.test.filters.Suppress; 33 import android.support.test.rule.ActivityTestRule; 34 import android.support.test.runner.AndroidJUnit4; 35 import androidx.recyclerview.widget.LinearLayoutManager; 36 import androidx.recyclerview.widget.RecyclerView; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.TextView; 41 42 import org.hamcrest.Matcher; 43 import org.junit.After; 44 import org.junit.Assume; 45 import org.junit.Before; 46 import org.junit.Rule; 47 import org.junit.Test; 48 import org.junit.runner.RunWith; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.Random; 53 54 import androidx.car.test.R; 55 56 /** Unit tests for the ability of the {@link PagedListView} to save state. */ 57 @RunWith(AndroidJUnit4.class) 58 @SmallTest 59 public final class PagedListViewSavedStateTest { 60 /** 61 * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of 62 * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items 63 * to ITEMS_PER_PAGE * desired_pages. 64 * 65 * <p>Actual value does not matter. 66 */ 67 private static final int ITEMS_PER_PAGE = 5; 68 69 /** 70 * The total number of items to display in a list. This value just needs to be large enough 71 * to ensure the scroll bar shows. 72 */ 73 private static final int TOTAL_ITEMS_IN_LIST = 100; 74 75 private static final int NUM_OF_PAGES = TOTAL_ITEMS_IN_LIST / ITEMS_PER_PAGE; 76 77 @Rule 78 public ActivityTestRule<PagedListViewSavedStateActivity> mActivityRule = 79 new ActivityTestRule<>(PagedListViewSavedStateActivity.class); 80 81 private PagedListViewSavedStateActivity mActivity; 82 private PagedListView mPagedListView1; 83 private PagedListView mPagedListView2; 84 85 @Before 86 public void setUp() { 87 Assume.assumeTrue(isAutoDevice()); 88 89 mActivity = mActivityRule.getActivity(); 90 mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 91 92 mPagedListView1 = mActivity.findViewById(R.id.paged_list_view_1); 93 mPagedListView2 = mActivity.findViewById(R.id.paged_list_view_2); 94 95 setUpPagedListView(mPagedListView1); 96 setUpPagedListView(mPagedListView2); 97 } 98 99 private boolean isAutoDevice() { 100 PackageManager packageManager = mActivityRule.getActivity().getPackageManager(); 101 return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 102 } 103 104 private void setUpPagedListView(PagedListView pagedListView) { 105 try { 106 mActivityRule.runOnUiThread(() -> { 107 pagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED); 108 pagedListView.setAdapter(new TestAdapter(TOTAL_ITEMS_IN_LIST, 109 pagedListView.getMeasuredHeight())); 110 }); 111 } catch (Throwable throwable) { 112 throwable.printStackTrace(); 113 throw new RuntimeException(throwable); 114 } 115 } 116 117 @After 118 public void tearDown() { 119 for (IdlingResource idlingResource : IdlingRegistry.getInstance().getResources()) { 120 IdlingRegistry.getInstance().unregister(idlingResource); 121 } 122 } 123 124 @Suppress 125 @Test 126 public void testPagePositionRememberedOnRotation() { 127 LinearLayoutManager layoutManager1 = 128 (LinearLayoutManager) mPagedListView1.getRecyclerView().getLayoutManager(); 129 LinearLayoutManager layoutManager2 = 130 (LinearLayoutManager) mPagedListView2.getRecyclerView().getLayoutManager(); 131 132 Random random = new Random(); 133 IdlingRegistry.getInstance().register(new PagedListViewScrollingIdlingResource( 134 mPagedListView1, mPagedListView2)); 135 136 // Add 1 to this random number to ensure it is a value between 1 and NUM_OF_PAGES. 137 int numOfClicks = 2; 138 clickPageDownButton(onPagedListView1(), numOfClicks); 139 int topPositionOfPagedListView1 = 140 layoutManager1.findFirstVisibleItemPosition(); 141 142 numOfClicks = 3; 143 clickPageDownButton(onPagedListView2(), numOfClicks); 144 int topPositionOfPagedListView2 = 145 layoutManager2.findFirstVisibleItemPosition(); 146 147 // Perform a configuration change by rotating the screen. 148 mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 149 mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 150 151 // Check that the positions are the same after the change. 152 assertEquals(topPositionOfPagedListView1, 153 layoutManager1.findFirstVisibleItemPosition()); 154 assertEquals(topPositionOfPagedListView2, 155 layoutManager2.findFirstVisibleItemPosition()); 156 } 157 158 /** Clicks the page down button on the given PagedListView for the given number of times. */ 159 private void clickPageDownButton(Matcher<View> pagedListView, int times) { 160 for (int i = 0; i < times; i++) { 161 onView(allOf(withId(R.id.page_down), pagedListView)).perform(click()); 162 } 163 } 164 165 166 /** Convenience method for checking that a View is on the first PagedListView. */ 167 private Matcher<View> onPagedListView1() { 168 return isDescendantOfA(withId(R.id.paged_list_view_1)); 169 } 170 171 /** Convenience method for checking that a View is on the second PagedListView. */ 172 private Matcher<View> onPagedListView2() { 173 return isDescendantOfA(withId(R.id.paged_list_view_2)); 174 } 175 176 private static String getItemText(int index) { 177 return "Data " + index; 178 } 179 180 /** An Adapter that ensures that there is {@link #ITEMS_PER_PAGE} displayed. */ 181 private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 182 implements PagedListView.ItemCap { 183 private List<String> mData; 184 private int mParentHeight; 185 186 TestAdapter(int itemCount, int parentHeight) { 187 mData = new ArrayList<>(); 188 for (int i = 0; i < itemCount; i++) { 189 mData.add(getItemText(i)); 190 } 191 mParentHeight = parentHeight; 192 } 193 194 @Override 195 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 196 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 197 return new TestViewHolder(inflater, parent); 198 } 199 200 @Override 201 public void onBindViewHolder(TestViewHolder holder, int position) { 202 // Calculate height for an item so one page fits ITEMS_PER_PAGE items. 203 int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE); 204 holder.itemView.setMinimumHeight(height); 205 holder.setText(mData.get(position)); 206 } 207 208 @Override 209 public int getItemCount() { 210 return mData.size(); 211 } 212 213 @Override 214 public void setMaxItems(int maxItems) { 215 // No-op 216 } 217 } 218 219 /** A ViewHolder that holds a View with a TextView. */ 220 private class TestViewHolder extends RecyclerView.ViewHolder { 221 private TextView mTextView; 222 223 TestViewHolder(LayoutInflater inflater, ViewGroup parent) { 224 super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false)); 225 mTextView = itemView.findViewById(R.id.text_view); 226 } 227 228 public void setText(String text) { 229 mTextView.setText(text); 230 } 231 } 232 233 // Registering IdlingResource in @Before method does not work - espresso doesn't actually wait 234 // for ViewAction to finish. So each method that clicks on button will need to register their 235 // own IdlingResource. 236 private class PagedListViewScrollingIdlingResource implements IdlingResource { 237 private boolean mIsIdle = true; 238 private ResourceCallback mResourceCallback; 239 240 PagedListViewScrollingIdlingResource(PagedListView pagedListView1, 241 PagedListView pagedListView2) { 242 // Ensure the IdlingResource waits for both RecyclerViews to finish their movement. 243 pagedListView1.getRecyclerView().addOnScrollListener(mOnScrollListener); 244 pagedListView2.getRecyclerView().addOnScrollListener(mOnScrollListener); 245 } 246 247 @Override 248 public String getName() { 249 return PagedListViewScrollingIdlingResource.class.getName(); 250 } 251 252 @Override 253 public boolean isIdleNow() { 254 return mIsIdle; 255 } 256 257 @Override 258 public void registerIdleTransitionCallback(ResourceCallback callback) { 259 mResourceCallback = callback; 260 } 261 262 private final RecyclerView.OnScrollListener mOnScrollListener = 263 new RecyclerView.OnScrollListener() { 264 @Override 265 public void onScrollStateChanged( 266 RecyclerView recyclerView, int newState) { 267 super.onScrollStateChanged(recyclerView, newState); 268 269 // Treat dragging as idle, or Espresso will block itself when 270 // swiping. 271 mIsIdle = (newState == RecyclerView.SCROLL_STATE_IDLE 272 || newState == RecyclerView.SCROLL_STATE_DRAGGING); 273 274 if (mIsIdle && mResourceCallback != null) { 275 mResourceCallback.onTransitionToIdle(); 276 } 277 } 278 279 @Override 280 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {} 281 }; 282 } 283 } 284