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