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 */ 16package androidx.fragment.app; 17 18import static org.junit.Assert.assertEquals; 19import static org.junit.Assert.assertFalse; 20import static org.junit.Assert.assertNotSame; 21import static org.junit.Assert.assertNull; 22import static org.junit.Assert.assertSame; 23import static org.junit.Assert.assertTrue; 24 25import android.app.Activity; 26import android.app.Instrumentation; 27import android.content.Intent; 28import android.os.Bundle; 29import android.os.SystemClock; 30import android.support.test.InstrumentationRegistry; 31import android.support.test.annotation.UiThreadTest; 32import android.support.test.filters.MediumTest; 33import android.support.test.rule.ActivityTestRule; 34import android.support.test.runner.AndroidJUnit4; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38 39import androidx.annotation.Nullable; 40import androidx.fragment.app.test.FragmentTestActivity; 41import androidx.fragment.app.test.NewIntentActivity; 42import androidx.fragment.test.R; 43 44import org.junit.After; 45import org.junit.Assert; 46import org.junit.Before; 47import org.junit.Rule; 48import org.junit.Test; 49import org.junit.runner.RunWith; 50 51import java.util.Collection; 52import java.util.concurrent.TimeUnit; 53 54/** 55 * Tests usage of the {@link FragmentTransaction} class. 56 */ 57@MediumTest 58@RunWith(AndroidJUnit4.class) 59public class FragmentTransactionTest { 60 61 @Rule 62 public ActivityTestRule<FragmentTestActivity> mActivityRule = 63 new ActivityTestRule<>(FragmentTestActivity.class); 64 65 private FragmentTestActivity mActivity; 66 private int mOnBackStackChangedTimes; 67 private FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener; 68 69 @Before 70 public void setUp() { 71 mActivity = mActivityRule.getActivity(); 72 mOnBackStackChangedTimes = 0; 73 mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() { 74 @Override 75 public void onBackStackChanged() { 76 mOnBackStackChangedTimes++; 77 } 78 }; 79 mActivity.getSupportFragmentManager() 80 .addOnBackStackChangedListener(mOnBackStackChangedListener); 81 } 82 83 @After 84 public void tearDown() { 85 mActivity.getSupportFragmentManager() 86 .removeOnBackStackChangedListener(mOnBackStackChangedListener); 87 mOnBackStackChangedListener = null; 88 } 89 90 @Test 91 public void testAddTransactionWithValidFragment() throws Throwable { 92 final Fragment fragment = new CorrectFragment(); 93 mActivityRule.runOnUiThread(new Runnable() { 94 @Override 95 public void run() { 96 mActivity.getSupportFragmentManager().beginTransaction() 97 .add(R.id.content, fragment) 98 .addToBackStack(null) 99 .commit(); 100 mActivity.getSupportFragmentManager().executePendingTransactions(); 101 assertEquals(1, mOnBackStackChangedTimes); 102 } 103 }); 104 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 105 assertTrue(fragment.isAdded()); 106 } 107 108 @Test 109 public void testAddTransactionWithPrivateFragment() throws Throwable { 110 final Fragment fragment = new PrivateFragment(); 111 mActivityRule.runOnUiThread(new Runnable() { 112 @Override 113 public void run() { 114 boolean exceptionThrown = false; 115 try { 116 mActivity.getSupportFragmentManager().beginTransaction() 117 .add(R.id.content, fragment) 118 .addToBackStack(null) 119 .commit(); 120 mActivity.getSupportFragmentManager().executePendingTransactions(); 121 assertEquals(1, mOnBackStackChangedTimes); 122 } catch (IllegalStateException e) { 123 exceptionThrown = true; 124 } finally { 125 assertTrue("Exception should be thrown", exceptionThrown); 126 assertFalse("Fragment shouldn't be added", fragment.isAdded()); 127 } 128 } 129 }); 130 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 131 } 132 133 @Test 134 public void testAddTransactionWithPackagePrivateFragment() throws Throwable { 135 final Fragment fragment = new PackagePrivateFragment(); 136 mActivityRule.runOnUiThread(new Runnable() { 137 @Override 138 public void run() { 139 boolean exceptionThrown = false; 140 try { 141 mActivity.getSupportFragmentManager().beginTransaction() 142 .add(R.id.content, fragment) 143 .addToBackStack(null) 144 .commit(); 145 mActivity.getSupportFragmentManager().executePendingTransactions(); 146 assertEquals(1, mOnBackStackChangedTimes); 147 } catch (IllegalStateException e) { 148 exceptionThrown = true; 149 } finally { 150 assertTrue("Exception should be thrown", exceptionThrown); 151 assertFalse("Fragment shouldn't be added", fragment.isAdded()); 152 } 153 } 154 }); 155 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 156 } 157 158 @Test 159 public void testAddTransactionWithAnonymousFragment() throws Throwable { 160 final Fragment fragment = new Fragment() {}; 161 mActivityRule.runOnUiThread(new Runnable() { 162 @Override 163 public void run() { 164 boolean exceptionThrown = false; 165 try { 166 mActivity.getSupportFragmentManager().beginTransaction() 167 .add(R.id.content, fragment) 168 .addToBackStack(null) 169 .commit(); 170 mActivity.getSupportFragmentManager().executePendingTransactions(); 171 assertEquals(1, mOnBackStackChangedTimes); 172 } catch (IllegalStateException e) { 173 exceptionThrown = true; 174 } finally { 175 assertTrue("Exception should be thrown", exceptionThrown); 176 assertFalse("Fragment shouldn't be added", fragment.isAdded()); 177 } 178 } 179 }); 180 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 181 } 182 183 @Test 184 public void testGetLayoutInflater() throws Throwable { 185 mActivityRule.runOnUiThread(new Runnable() { 186 @Override 187 public void run() { 188 final OnGetLayoutInflaterFragment fragment1 = new OnGetLayoutInflaterFragment(); 189 assertEquals(0, fragment1.onGetLayoutInflaterCalls); 190 mActivity.getSupportFragmentManager().beginTransaction() 191 .add(R.id.content, fragment1) 192 .addToBackStack(null) 193 .commit(); 194 mActivity.getSupportFragmentManager().executePendingTransactions(); 195 assertEquals(1, fragment1.onGetLayoutInflaterCalls); 196 assertEquals(fragment1.layoutInflater, fragment1.getLayoutInflater()); 197 // getLayoutInflater() didn't force onGetLayoutInflater() 198 assertEquals(1, fragment1.onGetLayoutInflaterCalls); 199 200 LayoutInflater layoutInflater = fragment1.layoutInflater; 201 // Replacing fragment1 won't detach it, so the value won't be cleared 202 final OnGetLayoutInflaterFragment fragment2 = new OnGetLayoutInflaterFragment(); 203 mActivity.getSupportFragmentManager().beginTransaction() 204 .replace(R.id.content, fragment2) 205 .addToBackStack(null) 206 .commit(); 207 mActivity.getSupportFragmentManager().executePendingTransactions(); 208 209 assertSame(layoutInflater, fragment1.getLayoutInflater()); 210 assertEquals(1, fragment1.onGetLayoutInflaterCalls); 211 212 // Popping it should cause onCreateView again, so a new LayoutInflater... 213 mActivity.getSupportFragmentManager().popBackStackImmediate(); 214 assertNotSame(layoutInflater, fragment1.getLayoutInflater()); 215 assertEquals(2, fragment1.onGetLayoutInflaterCalls); 216 layoutInflater = fragment1.layoutInflater; 217 assertSame(layoutInflater, fragment1.getLayoutInflater()); 218 219 // Popping it should detach it, clearing the cached value again 220 mActivity.getSupportFragmentManager().popBackStackImmediate(); 221 222 // once it is detached, the getLayoutInflater() will default to throw 223 // an exception, but we've made it return null instead. 224 assertEquals(2, fragment1.onGetLayoutInflaterCalls); 225 assertNull(fragment1.getLayoutInflater()); 226 assertEquals(3, fragment1.onGetLayoutInflaterCalls); 227 } 228 }); 229 } 230 231 @Test 232 public void testAddTransactionWithNonStaticFragment() throws Throwable { 233 final Fragment fragment = new NonStaticFragment(); 234 mActivityRule.runOnUiThread(new Runnable() { 235 @Override 236 public void run() { 237 boolean exceptionThrown = false; 238 try { 239 mActivity.getSupportFragmentManager().beginTransaction() 240 .add(R.id.content, fragment) 241 .addToBackStack(null) 242 .commit(); 243 mActivity.getSupportFragmentManager().executePendingTransactions(); 244 assertEquals(1, mOnBackStackChangedTimes); 245 } catch (IllegalStateException e) { 246 exceptionThrown = true; 247 } finally { 248 assertTrue("Exception should be thrown", exceptionThrown); 249 assertFalse("Fragment shouldn't be added", fragment.isAdded()); 250 } 251 } 252 }); 253 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 254 } 255 256 @Test 257 public void testPostOnCommit() throws Throwable { 258 mActivityRule.runOnUiThread(new Runnable() { 259 @Override 260 public void run() { 261 final boolean[] ran = new boolean[1]; 262 FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager(); 263 fm.beginTransaction().runOnCommit(new Runnable() { 264 @Override 265 public void run() { 266 ran[0] = true; 267 } 268 }).commit(); 269 fm.executePendingTransactions(); 270 271 assertTrue("runOnCommit runnable never ran", ran[0]); 272 273 ran[0] = false; 274 275 boolean threw = false; 276 try { 277 fm.beginTransaction().runOnCommit(new Runnable() { 278 @Override 279 public void run() { 280 ran[0] = true; 281 } 282 }).addToBackStack(null).commit(); 283 } catch (IllegalStateException ise) { 284 threw = true; 285 } 286 287 fm.executePendingTransactions(); 288 289 assertTrue("runOnCommit was allowed to be called for back stack transaction", 290 threw); 291 assertFalse("runOnCommit runnable for back stack transaction was run", ran[0]); 292 } 293 }); 294 } 295 296 /** 297 * Test to ensure that when onBackPressed() is received that there is no crash. 298 */ 299 @Test 300 @UiThreadTest 301 public void crashOnBackPressed() throws Throwable { 302 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 303 Bundle outState = new Bundle(); 304 FragmentTestActivity activity = mActivityRule.getActivity(); 305 instrumentation.callActivityOnSaveInstanceState(activity, outState); 306 activity.onBackPressed(); 307 } 308 309 // Ensure that getFragments() works during transactions, even if it is run off thread 310 @Test 311 public void getFragmentsOffThread() throws Throwable { 312 final FragmentManager fm = mActivity.getSupportFragmentManager(); 313 314 // Make sure that adding a fragment works 315 Fragment fragment = new CorrectFragment(); 316 fm.beginTransaction() 317 .add(R.id.content, fragment) 318 .addToBackStack(null) 319 .commit(); 320 321 FragmentTestUtil.executePendingTransactions(mActivityRule); 322 Collection<Fragment> fragments = fm.getFragments(); 323 assertEquals(1, fragments.size()); 324 assertTrue(fragments.contains(fragment)); 325 326 // Removed fragments shouldn't show 327 fm.beginTransaction() 328 .remove(fragment) 329 .addToBackStack(null) 330 .commit(); 331 FragmentTestUtil.executePendingTransactions(mActivityRule); 332 assertTrue(fm.getFragments().isEmpty()); 333 334 // Now try detached fragments 335 FragmentTestUtil.popBackStackImmediate(mActivityRule); 336 fm.beginTransaction() 337 .detach(fragment) 338 .addToBackStack(null) 339 .commit(); 340 FragmentTestUtil.executePendingTransactions(mActivityRule); 341 assertTrue(fm.getFragments().isEmpty()); 342 343 // Now try hidden fragments 344 FragmentTestUtil.popBackStackImmediate(mActivityRule); 345 fm.beginTransaction() 346 .hide(fragment) 347 .addToBackStack(null) 348 .commit(); 349 FragmentTestUtil.executePendingTransactions(mActivityRule); 350 fragments = fm.getFragments(); 351 assertEquals(1, fragments.size()); 352 assertTrue(fragments.contains(fragment)); 353 354 // And showing it again shouldn't change anything: 355 FragmentTestUtil.popBackStackImmediate(mActivityRule); 356 fragments = fm.getFragments(); 357 assertEquals(1, fragments.size()); 358 assertTrue(fragments.contains(fragment)); 359 360 // Now pop back to the start state 361 FragmentTestUtil.popBackStackImmediate(mActivityRule); 362 363 // We can't force concurrency, but we can do it lots of times and hope that 364 // we hit it. 365 for (int i = 0; i < 100; i++) { 366 Fragment fragment2 = new CorrectFragment(); 367 fm.beginTransaction() 368 .add(R.id.content, fragment2) 369 .addToBackStack(null) 370 .commit(); 371 getFragmentsUntilSize(1); 372 373 fm.popBackStack(); 374 getFragmentsUntilSize(0); 375 } 376 } 377 378 /** 379 * When a FragmentManager is detached, it should allow commitAllowingStateLoss() 380 * and commitNowAllowingStateLoss() by just dropping the transaction. 381 */ 382 @Test 383 public void commitAllowStateLossDetached() throws Throwable { 384 Fragment fragment1 = new CorrectFragment(); 385 mActivity.getSupportFragmentManager() 386 .beginTransaction() 387 .add(fragment1, "1") 388 .commit(); 389 FragmentTestUtil.executePendingTransactions(mActivityRule); 390 final FragmentManager fm = fragment1.getChildFragmentManager(); 391 mActivity.getSupportFragmentManager() 392 .beginTransaction() 393 .remove(fragment1) 394 .commit(); 395 FragmentTestUtil.executePendingTransactions(mActivityRule); 396 Assert.assertEquals(0, mActivity.getSupportFragmentManager().getFragments().size()); 397 assertEquals(0, fm.getFragments().size()); 398 399 // Now the fragment1's fragment manager should allow commitAllowingStateLoss 400 // by doing nothing since it has been detached. 401 Fragment fragment2 = new CorrectFragment(); 402 fm.beginTransaction() 403 .add(fragment2, "2") 404 .commitAllowingStateLoss(); 405 FragmentTestUtil.executePendingTransactions(mActivityRule); 406 assertEquals(0, fm.getFragments().size()); 407 408 // It should also allow commitNowAllowingStateLoss by doing nothing 409 mActivityRule.runOnUiThread(new Runnable() { 410 @Override 411 public void run() { 412 Fragment fragment3 = new CorrectFragment(); 413 fm.beginTransaction() 414 .add(fragment3, "3") 415 .commitNowAllowingStateLoss(); 416 assertEquals(0, fm.getFragments().size()); 417 } 418 }); 419 } 420 421 /** 422 * onNewIntent() should note that the state is not saved so that child fragment 423 * managers can execute transactions. 424 */ 425 @Test 426 public void newIntentUnlocks() throws Throwable { 427 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 428 Intent intent1 = new Intent(mActivity, NewIntentActivity.class) 429 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 430 NewIntentActivity newIntentActivity = 431 (NewIntentActivity) instrumentation.startActivitySync(intent1); 432 FragmentTestUtil.waitForExecution(mActivityRule); 433 434 Intent intent2 = new Intent(mActivity, FragmentTestActivity.class); 435 intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 436 Activity coveringActivity = instrumentation.startActivitySync(intent2); 437 FragmentTestUtil.waitForExecution(mActivityRule); 438 439 Intent intent3 = new Intent(mActivity, NewIntentActivity.class) 440 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 441 mActivity.startActivity(intent3); 442 assertTrue(newIntentActivity.newIntent.await(1, TimeUnit.SECONDS)); 443 FragmentTestUtil.waitForExecution(mActivityRule); 444 445 for (Fragment fragment : newIntentActivity.getSupportFragmentManager().getFragments()) { 446 // There really should only be one fragment in newIntentActivity. 447 assertEquals(1, fragment.getChildFragmentManager().getFragments().size()); 448 } 449 } 450 451 private void getFragmentsUntilSize(int expectedSize) { 452 final long endTime = SystemClock.uptimeMillis() + 3000; 453 454 do { 455 assertTrue(SystemClock.uptimeMillis() < endTime); 456 } while (mActivity.getSupportFragmentManager().getFragments().size() != expectedSize); 457 } 458 459 public static class CorrectFragment extends Fragment {} 460 461 private static class PrivateFragment extends Fragment {} 462 463 static class PackagePrivateFragment extends Fragment {} 464 465 private class NonStaticFragment extends Fragment {} 466 467 public static class OnGetLayoutInflaterFragment extends Fragment { 468 public int onGetLayoutInflaterCalls = 0; 469 public LayoutInflater layoutInflater; 470 471 @Override 472 public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) { 473 onGetLayoutInflaterCalls++; 474 try { 475 layoutInflater = super.onGetLayoutInflater(savedInstanceState); 476 } catch (Exception e) { 477 return null; 478 } 479 return layoutInflater; 480 } 481 482 @Nullable 483 @Override 484 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 485 @Nullable Bundle savedInstanceState) { 486 return inflater.inflate(R.layout.fragment_a, container, false); 487 } 488 } 489} 490