1 /* 2 * Copyright (C) 2016 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 package androidx.leanback.app; 17 18 import static org.junit.Assert.assertEquals; 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertTrue; 21 import static org.mockito.ArgumentMatchers.nullable; 22 import static org.mockito.Mockito.any; 23 import static org.mockito.Mockito.doAnswer; 24 import static org.mockito.Mockito.timeout; 25 import static org.mockito.Mockito.times; 26 import static org.mockito.Mockito.verify; 27 28 import android.os.Bundle; 29 import android.support.test.InstrumentationRegistry; 30 import android.support.test.filters.LargeTest; 31 import android.support.test.runner.AndroidJUnit4; 32 import android.view.KeyEvent; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 37 import androidx.leanback.testutils.PollingCheck; 38 import androidx.leanback.widget.GuidedAction; 39 import androidx.leanback.widget.GuidedActionsStylist; 40 import androidx.leanback.widget.VerticalGridView; 41 import androidx.recyclerview.widget.DefaultItemAnimator; 42 import androidx.recyclerview.widget.RecyclerView; 43 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 import org.mockito.ArgumentCaptor; 47 import org.mockito.invocation.InvocationOnMock; 48 import org.mockito.stubbing.Answer; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 53 @LargeTest 54 @RunWith(AndroidJUnit4.class) 55 public class GuidedStepSupportFragmentTest extends GuidedStepSupportFragmentTestBase { 56 57 private static final int ON_DESTROY_TIMEOUT = 5000; 58 59 @Test 60 public void nextAndBack() throws Throwable { 61 final String firstFragmentName = generateMethodTestName("first"); 62 final String secondFragmentName = generateMethodTestName("second"); 63 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 64 doAnswer(new Answer<Void>() { 65 @Override 66 public Void answer(InvocationOnMock invocation) { 67 List actions = (List) invocation.getArguments()[0]; 68 actions.add(new GuidedAction.Builder().id(1000).title("OK").build()); 69 return null; 70 } 71 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 72 doAnswer(new Answer<Void>() { 73 @Override 74 public Void answer(InvocationOnMock invocation) { 75 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 76 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 77 invocation.getMock(); 78 if (action.getId() == 1000) { 79 GuidedStepSupportFragment.add(obj.getFragmentManager(), 80 new GuidedStepTestSupportFragment(secondFragmentName)); 81 } 82 return null; 83 } 84 }).when(first).onGuidedActionClicked(any(GuidedAction.class)); 85 86 GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName); 87 88 GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName); 89 verify(first, times(1)).onCreate(nullable(Bundle.class)); 90 verify(first, times(1)).onCreateGuidance(nullable(Bundle.class)); 91 verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class)); 92 verify(first, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class)); 93 verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 94 nullable(Bundle.class), any(View.class)); 95 verify(first, times(1)).onViewStateRestored(nullable(Bundle.class)); 96 verify(first, times(1)).onStart(); 97 verify(first, times(1)).onResume(); 98 99 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 100 verify(first, times(1)).onGuidedActionClicked(any(GuidedAction.class)); 101 102 PollingCheck.waitFor(new EnterTransitionFinish(second)); 103 verify(first, times(1)).onPause(); 104 verify(first, times(1)).onStop(); 105 verify(first, times(1)).onDestroyView(); 106 verify(second, times(1)).onCreate(nullable(Bundle.class)); 107 verify(second, times(1)).onCreateGuidance(nullable(Bundle.class)); 108 verify(second, times(1)).onCreateActions(any(List.class), nullable(Bundle.class)); 109 verify(second, times(1)).onCreateButtonActions(any(List.class), nullable(Bundle.class)); 110 verify(second, times(1)).onCreateView(any(LayoutInflater.class), nullable(ViewGroup.class), 111 nullable(Bundle.class), any(View.class)); 112 verify(second, times(1)).onViewStateRestored(nullable(Bundle.class)); 113 verify(second, times(1)).onStart(); 114 verify(second, times(1)).onResume(); 115 116 sendKey(KeyEvent.KEYCODE_BACK); 117 118 PollingCheck.waitFor(new EnterTransitionFinish(first)); 119 verify(second, times(1)).onPause(); 120 verify(second, times(1)).onStop(); 121 verify(second, times(1)).onDestroyView(); 122 verify(second, times(1)).onDestroy(); 123 verify(first, times(1)).onCreateActions(any(List.class), nullable(Bundle.class)); 124 verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 125 nullable(Bundle.class), any(View.class)); 126 verify(first, times(2)).onViewStateRestored(nullable(Bundle.class)); 127 verify(first, times(2)).onStart(); 128 verify(first, times(2)).onResume(); 129 130 sendKey(KeyEvent.KEYCODE_BACK); 131 PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity)); 132 verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy(); 133 assertTrue(activity.isDestroyed()); 134 } 135 136 @Test 137 public void restoreFragments() throws Throwable { 138 final String firstFragmentName = generateMethodTestName("first"); 139 final String secondFragmentName = generateMethodTestName("second"); 140 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 141 doAnswer(new Answer<Void>() { 142 @Override 143 public Void answer(InvocationOnMock invocation) { 144 List actions = (List) invocation.getArguments()[0]; 145 actions.add(new GuidedAction.Builder().id(1000).title("OK").build()); 146 actions.add(new GuidedAction.Builder().id(1001).editable(true).title("text") 147 .build()); 148 actions.add(new GuidedAction.Builder().id(1002).editable(true).title("text") 149 .autoSaveRestoreEnabled(false).build()); 150 return null; 151 } 152 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 153 doAnswer(new Answer<Void>() { 154 @Override 155 public Void answer(InvocationOnMock invocation) { 156 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 157 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 158 invocation.getMock(); 159 if (action.getId() == 1000) { 160 GuidedStepSupportFragment.add(obj.getFragmentManager(), 161 new GuidedStepTestSupportFragment(secondFragmentName)); 162 } 163 return null; 164 } 165 }).when(first).onGuidedActionClicked(any(GuidedAction.class)); 166 167 GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName); 168 169 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName); 170 first.getFragment().findActionById(1001).setTitle("modified text"); 171 first.getFragment().findActionById(1002).setTitle("modified text"); 172 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 173 PollingCheck.waitFor(new EnterTransitionFinish(second)); 174 175 activityTestRule.runOnUiThread(new Runnable() { 176 @Override 177 public void run() { 178 activity.recreate(); 179 } 180 }); 181 PollingCheck.waitFor(new EnterTransitionFinish(second)); 182 verify(first, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 183 nullable(Bundle.class), any(View.class)); 184 verify(first, times(1)).onDestroy(); 185 verify(second, times(2)).onCreate(nullable(Bundle.class)); 186 verify(second, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 187 nullable(Bundle.class), any(View.class)); 188 verify(second, times(1)).onDestroy(); 189 190 sendKey(KeyEvent.KEYCODE_BACK); 191 PollingCheck.waitFor(new EnterTransitionFinish(first)); 192 verify(second, times(2)).onPause(); 193 verify(second, times(2)).onStop(); 194 verify(second, times(2)).onDestroyView(); 195 verify(second, times(2)).onDestroy(); 196 assertEquals("modified text", first.getFragment().findActionById(1001).getTitle()); 197 assertEquals("text", first.getFragment().findActionById(1002).getTitle()); 198 verify(first, times(2)).onCreate(nullable(Bundle.class)); 199 verify(first, times(2)).onCreateActions(any(List.class), nullable(Bundle.class)); 200 verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 201 nullable(Bundle.class), any(View.class)); 202 } 203 204 205 @Test 206 public void finishGuidedStepSupportFragment_finishes_activity() throws Throwable { 207 final String firstFragmentName = generateMethodTestName("first"); 208 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 209 doAnswer(new Answer<Void>() { 210 @Override 211 public Void answer(InvocationOnMock invocation) { 212 List actions = (List) invocation.getArguments()[0]; 213 actions.add(new GuidedAction.Builder().id(1001).title("Finish activity").build()); 214 return null; 215 } 216 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 217 doAnswer(new Answer<Void>() { 218 @Override 219 public Void answer(InvocationOnMock invocation) { 220 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 221 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 222 invocation.getMock(); 223 if (action.getId() == 1001) { 224 obj.getFragment().finishGuidedStepSupportFragments(); 225 } 226 return null; 227 } 228 }).when(first).onGuidedActionClicked(any(GuidedAction.class)); 229 230 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName); 231 232 View viewFinish = first.getFragment().getActionItemView(0); 233 assertTrue(viewFinish.hasFocus()); 234 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 235 PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity)); 236 verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy(); 237 } 238 239 @Test 240 public void finishGuidedStepSupportFragment_finishes_fragments() throws Throwable { 241 final String firstFragmentName = generateMethodTestName("first"); 242 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 243 doAnswer(new Answer<Void>() { 244 @Override 245 public Void answer(InvocationOnMock invocation) { 246 List actions = (List) invocation.getArguments()[0]; 247 actions.add(new GuidedAction.Builder().id(1001).title("Finish fragments").build()); 248 return null; 249 } 250 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 251 doAnswer(new Answer<Void>() { 252 @Override 253 public Void answer(InvocationOnMock invocation) { 254 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 255 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 256 invocation.getMock(); 257 if (action.getId() == 1001) { 258 obj.getFragment().finishGuidedStepSupportFragments(); 259 } 260 return null; 261 } 262 }).when(first).onGuidedActionClicked(any(GuidedAction.class)); 263 264 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName, 265 false /*asRoot*/); 266 267 View viewFinish = first.getFragment().getActionItemView(0); 268 assertTrue(viewFinish.hasFocus()); 269 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 270 271 // fragment should be destroyed, activity should not destroyed 272 waitOnDestroy(first, 1); 273 assertFalse(activity.isDestroyed()); 274 } 275 276 @Test 277 public void subActions() throws Throwable { 278 final String firstFragmentName = generateMethodTestName("first"); 279 final String secondFragmentName = generateMethodTestName("second"); 280 final boolean[] expandSubActionInOnCreateView = new boolean[] {false}; 281 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 282 doAnswer(new Answer<Void>() { 283 @Override 284 public Void answer(InvocationOnMock invocation) { 285 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 286 invocation.getMock(); 287 if (expandSubActionInOnCreateView[0]) { 288 obj.getFragment().expandAction(obj.getFragment().findActionById(1000), false); 289 } 290 return null; 291 } 292 }).when(first).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 293 nullable(Bundle.class), any(View.class)); 294 doAnswer(new Answer<Void>() { 295 @Override 296 public Void answer(InvocationOnMock invocation) { 297 List actions = (List) invocation.getArguments()[0]; 298 List<GuidedAction> subActions = new ArrayList<GuidedAction>(); 299 subActions.add(new GuidedAction.Builder().id(2000).title("item1").build()); 300 subActions.add(new GuidedAction.Builder().id(2001).title("item2").build()); 301 actions.add(new GuidedAction.Builder().id(1000).subActions(subActions) 302 .title("list").build()); 303 return null; 304 } 305 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 306 doAnswer(new Answer<Boolean>() { 307 @Override 308 public Boolean answer(InvocationOnMock invocation) { 309 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 310 invocation.getMock(); 311 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 312 if (action.getId() == 2000) { 313 return true; 314 } else if (action.getId() == 2001) { 315 GuidedStepSupportFragment.add(obj.getFragmentManager(), 316 new GuidedStepTestSupportFragment(secondFragmentName)); 317 return false; 318 } 319 return false; 320 } 321 }).when(first).onSubGuidedActionClicked(any(GuidedAction.class)); 322 323 GuidedStepTestSupportFragment.Provider second = mockProvider(secondFragmentName); 324 325 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName); 326 327 // after clicked, it sub actions list should expand 328 View viewForList = first.getFragment().getActionItemView(0); 329 assertTrue(viewForList.hasFocus()); 330 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 331 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 332 assertFalse(viewForList.hasFocus()); 333 334 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 335 ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class); 336 verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture()); 337 assertEquals(2000, actionCapture.getValue().getId()); 338 // after clicked a sub action, it sub actions list should close 339 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 340 assertTrue(viewForList.hasFocus()); 341 342 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 343 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 344 345 assertFalse(viewForList.hasFocus()); 346 sendKey(KeyEvent.KEYCODE_DPAD_DOWN); 347 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 348 ArgumentCaptor<GuidedAction> actionCapture2 = ArgumentCaptor.forClass(GuidedAction.class); 349 verify(first, times(2)).onSubGuidedActionClicked(actionCapture2.capture()); 350 assertEquals(2001, actionCapture2.getValue().getId()); 351 352 PollingCheck.waitFor(new EnterTransitionFinish(second)); 353 verify(second, times(1)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 354 nullable(Bundle.class), any(View.class)); 355 356 // test expand sub action when return to first fragment 357 expandSubActionInOnCreateView[0] = true; 358 sendKey(KeyEvent.KEYCODE_BACK); 359 PollingCheck.waitFor(new EnterTransitionFinish(first)); 360 verify(first, times(2)).onCreateView(any(LayoutInflater.class), any(ViewGroup.class), 361 nullable(Bundle.class), any(View.class)); 362 assertTrue(first.getFragment().isExpanded()); 363 364 sendKey(KeyEvent.KEYCODE_BACK); 365 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 366 assertFalse(first.getFragment().isExpanded()); 367 368 sendKey(KeyEvent.KEYCODE_BACK); 369 PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity)); 370 verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy(); 371 } 372 373 @Test 374 public void setActionsWhenSubActionsExpanded() throws Throwable { 375 final String firstFragmentName = generateMethodTestName("first"); 376 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 377 doAnswer(new Answer<Void>() { 378 @Override 379 public Void answer(InvocationOnMock invocation) { 380 List actions = (List) invocation.getArguments()[0]; 381 List<GuidedAction> subActions = new ArrayList<GuidedAction>(); 382 subActions.add(new GuidedAction.Builder().id(2000).title("item1").build()); 383 actions.add(new GuidedAction.Builder().id(1000).subActions(subActions) 384 .title("list").build()); 385 return null; 386 } 387 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 388 doAnswer(new Answer<Boolean>() { 389 @Override 390 public Boolean answer(InvocationOnMock invocation) { 391 GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider) 392 invocation.getMock(); 393 GuidedAction action = (GuidedAction) invocation.getArguments()[0]; 394 if (action.getId() == 2000) { 395 List<GuidedAction> newActions = new ArrayList<GuidedAction>(); 396 newActions.add(new GuidedAction.Builder().id(1001).title("item2").build()); 397 obj.getFragment().setActions(newActions); 398 return false; 399 } 400 return false; 401 } 402 }).when(first).onSubGuidedActionClicked(any(GuidedAction.class)); 403 404 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName); 405 406 // after clicked, it sub actions list should expand 407 View firstView = first.getFragment().getActionItemView(0); 408 assertTrue(firstView.hasFocus()); 409 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 410 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 411 assertFalse(firstView.hasFocus()); 412 413 sendKey(KeyEvent.KEYCODE_DPAD_CENTER); 414 ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class); 415 verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture()); 416 // after clicked a sub action, whole action list is replaced. 417 PollingCheck.waitFor(new ExpandTransitionFinish(first)); 418 assertFalse(first.getFragment().isExpanded()); 419 View newFirstView = first.getFragment().getActionItemView(0); 420 assertTrue(newFirstView.hasFocus()); 421 assertTrue(newFirstView.getVisibility() == View.VISIBLE); 422 GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment() 423 .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView); 424 assertEquals(1001, vh.getAction().getId()); 425 426 } 427 428 @Test 429 public void buttonActionsRtl() throws Throwable { 430 final String firstFragmentName = generateMethodTestName("first"); 431 GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 432 doAnswer(new Answer<Void>() { 433 @Override 434 public Void answer(InvocationOnMock invocation) { 435 List actions = (List) invocation.getArguments()[0]; 436 actions.add(new GuidedAction.Builder().id(1000).title("action").build()); 437 return null; 438 } 439 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 440 doAnswer(new Answer<Void>() { 441 @Override 442 public Void answer(InvocationOnMock invocation) { 443 List actions = (List) invocation.getArguments()[0]; 444 actions.add(new GuidedAction.Builder().id(1001).title("button action").build()); 445 return null; 446 } 447 }).when(first).onCreateButtonActions(any(List.class), nullable(Bundle.class)); 448 449 final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName, 450 true, View.LAYOUT_DIRECTION_RTL); 451 452 assertEquals(View.LAYOUT_DIRECTION_RTL, first.getFragment().getView().getLayoutDirection()); 453 View firstView = first.getFragment().getActionItemView(0); 454 assertTrue(firstView.hasFocus()); 455 } 456 457 @Test 458 public void recyclerViewDiffTest() throws Throwable { 459 final String firstFragmentName = generateMethodTestName("first"); 460 final GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName); 461 doAnswer(new Answer<Void>() { 462 @Override 463 public Void answer(InvocationOnMock invocation) { 464 List actions = (List) invocation.getArguments()[0]; 465 actions.add(new GuidedAction.Builder().id(1000).title("action1").build()); 466 actions.add(new GuidedAction.Builder().id(1001).title("action2").build()); 467 return null; 468 } 469 }).when(first).onCreateActions(any(List.class), nullable(Bundle.class)); 470 471 launchTestActivity(firstFragmentName, true); 472 473 final ArrayList<RecyclerView.ViewHolder> changeList = new ArrayList(); 474 VerticalGridView rv = first.getFragment().mActionsStylist.getActionsGridView(); 475 rv.setItemAnimator(new DefaultItemAnimator() { 476 @Override 477 public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) { 478 if (!oldItem) { 479 changeList.add(item); 480 } 481 super.onChangeStarting(item, oldItem); 482 } 483 }); 484 485 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 486 @Override 487 public void run() { 488 List actions = new ArrayList(); 489 actions.add(new GuidedAction.Builder().id(1001).title("action2x").build()); 490 actions.add(new GuidedAction.Builder().id(1000).title("action1x").build()); 491 first.getFragment().setActions(actions); 492 } 493 }); 494 495 // should causes two change animation. 496 PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() { 497 @Override 498 public boolean canProceed() { 499 return changeList.size() == 2; 500 } 501 }); 502 } 503 } 504