FragmentLifecycleTest.java revision 96cd95c37930d6d8f79ee8068991f9686c884f7a
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
17
18package android.support.v4.app;
19
20import android.content.Intent;
21import android.os.Bundle;
22import android.os.Parcelable;
23import android.support.annotation.NonNull;
24import android.support.annotation.Nullable;
25import android.support.test.annotation.UiThreadTest;
26import android.support.test.rule.ActivityTestRule;
27import android.support.test.runner.AndroidJUnit4;
28import android.support.v4.app.test.EmptyFragmentTestActivity;
29import android.support.v4.test.R;
30import android.test.suitebuilder.annotation.MediumTest;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.Window;
35
36import org.junit.Assert;
37import org.junit.Rule;
38import org.junit.Test;
39import org.junit.runner.RunWith;
40
41import java.io.FileDescriptor;
42import java.io.PrintWriter;
43
44import static junit.framework.Assert.assertEquals;
45import static junit.framework.Assert.assertFalse;
46import static junit.framework.Assert.assertNotNull;
47import static junit.framework.Assert.assertNotSame;
48import static junit.framework.Assert.assertNull;
49import static junit.framework.Assert.assertSame;
50import static junit.framework.Assert.assertTrue;
51import static org.junit.Assert.assertNotEquals;
52
53@RunWith(AndroidJUnit4.class)
54@MediumTest
55public class FragmentLifecycleTest {
56
57    @Rule
58    public ActivityTestRule<EmptyFragmentTestActivity> mActivityRule =
59            new ActivityTestRule<EmptyFragmentTestActivity>(EmptyFragmentTestActivity.class);
60
61    @Test
62    public void basicLifecycle() throws Throwable {
63        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
64        final StrictFragment strictFragment = new StrictFragment();
65
66        // Add fragment; StrictFragment will throw if it detects any violation
67        // in standard lifecycle method ordering or expected preconditions.
68        fm.beginTransaction().add(strictFragment, "EmptyHeadless").commit();
69        executePendingTransactions(fm);
70
71        assertTrue("fragment is not added", strictFragment.isAdded());
72        assertFalse("fragment is detached", strictFragment.isDetached());
73        assertTrue("fragment is not resumed", strictFragment.isResumed());
74
75        // Test removal as well; StrictFragment will throw here too.
76        fm.beginTransaction().remove(strictFragment).commit();
77        executePendingTransactions(fm);
78
79        assertFalse("fragment is added", strictFragment.isAdded());
80        assertFalse("fragment is resumed", strictFragment.isResumed());
81
82        // This one is perhaps counterintuitive; "detached" means specifically detached
83        // but still managed by a FragmentManager. The .remove call above
84        // should not enter this state.
85        assertFalse("fragment is detached", strictFragment.isDetached());
86    }
87
88    @Test
89    public void detachment() throws Throwable {
90        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
91        final StrictFragment f1 = new StrictFragment();
92        final StrictFragment f2 = new StrictFragment();
93
94        fm.beginTransaction().add(f1, "1").add(f2, "2").commit();
95        executePendingTransactions(fm);
96
97        assertTrue("fragment 1 is not added", f1.isAdded());
98        assertTrue("fragment 2 is not added", f2.isAdded());
99
100        // Test detaching fragments using StrictFragment to throw on errors.
101        fm.beginTransaction().detach(f1).detach(f2).commit();
102        executePendingTransactions(fm);
103
104        assertTrue("fragment 1 is not detached", f1.isDetached());
105        assertTrue("fragment 2 is not detached", f2.isDetached());
106        assertFalse("fragment 1 is added", f1.isAdded());
107        assertFalse("fragment 2 is added", f2.isAdded());
108
109        // Only reattach f1; leave v2 detached.
110        fm.beginTransaction().attach(f1).commit();
111        executePendingTransactions(fm);
112
113        assertTrue("fragment 1 is not added", f1.isAdded());
114        assertFalse("fragment 1 is detached", f1.isDetached());
115        assertTrue("fragment 2 is not detached", f2.isDetached());
116
117        // Remove both from the FragmentManager.
118        fm.beginTransaction().remove(f1).remove(f2).commit();
119        executePendingTransactions(fm);
120
121        assertFalse("fragment 1 is added", f1.isAdded());
122        assertFalse("fragment 2 is added", f2.isAdded());
123        assertFalse("fragment 1 is detached", f1.isDetached());
124        assertFalse("fragment 2 is detached", f2.isDetached());
125    }
126
127    @Test
128    public void basicBackStack() throws Throwable {
129        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
130        final StrictFragment f1 = new StrictFragment();
131        final StrictFragment f2 = new StrictFragment();
132
133        // Add a fragment normally to set up
134        fm.beginTransaction().add(f1, "1").commit();
135        executePendingTransactions(fm);
136
137        assertTrue("fragment 1 is not added", f1.isAdded());
138
139        // Remove the first one and add a second. We're not using replace() here since
140        // these fragments are headless and as of this test writing, replace() only works
141        // for fragments with views and a container view id.
142        // Add it to the back stack so we can pop it afterwards.
143        fm.beginTransaction().remove(f1).add(f2, "2").addToBackStack("stack1").commit();
144        executePendingTransactions(fm);
145
146        assertFalse("fragment 1 is added", f1.isAdded());
147        assertTrue("fragment 2 is not added", f2.isAdded());
148
149        // Test popping the stack
150        fm.popBackStack();
151        executePendingTransactions(fm);
152
153        assertFalse("fragment 2 is added", f2.isAdded());
154        assertTrue("fragment 1 is not added", f1.isAdded());
155    }
156
157    @Test
158    public void attachBackStack() throws Throwable {
159        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
160        final StrictFragment f1 = new StrictFragment();
161        final StrictFragment f2 = new StrictFragment();
162
163        // Add a fragment normally to set up
164        fm.beginTransaction().add(f1, "1").commit();
165        executePendingTransactions(fm);
166
167        assertTrue("fragment 1 is not added", f1.isAdded());
168
169        fm.beginTransaction().detach(f1).add(f2, "2").addToBackStack("stack1").commit();
170        executePendingTransactions(fm);
171
172        assertTrue("fragment 1 is not detached", f1.isDetached());
173        assertFalse("fragment 2 is detached", f2.isDetached());
174        assertFalse("fragment 1 is added", f1.isAdded());
175        assertTrue("fragment 2 is not added", f2.isAdded());
176    }
177
178    @Test
179    public void viewLifecycle() throws Throwable {
180        // Test basic lifecycle when the fragment creates a view
181
182        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
183        final StrictViewFragment f1 = new StrictViewFragment();
184
185        fm.beginTransaction().add(android.R.id.content, f1).commit();
186        executePendingTransactions(fm);
187
188        assertTrue("fragment 1 is not added", f1.isAdded());
189        final View view = f1.getView();
190        assertNotNull("fragment 1 returned null from getView", view);
191        assertTrue("fragment 1's view is not attached to a window", view.isAttachedToWindow());
192
193        fm.beginTransaction().remove(f1).commit();
194        executePendingTransactions(fm);
195
196        assertFalse("fragment 1 is added", f1.isAdded());
197        assertNull("fragment 1 returned non-null from getView after removal", f1.getView());
198        assertFalse("fragment 1's previous view is still attached to a window",
199                view.isAttachedToWindow());
200    }
201
202    @Test
203    public void viewReplace() throws Throwable {
204        // Replace one view with another, then reverse it with the back stack
205
206        final FragmentManager fm = mActivityRule.getActivity().getSupportFragmentManager();
207        final StrictViewFragment f1 = new StrictViewFragment();
208        final StrictViewFragment f2 = new StrictViewFragment();
209
210        fm.beginTransaction().add(android.R.id.content, f1).commit();
211        executePendingTransactions(fm);
212
213        assertTrue("fragment 1 is not added", f1.isAdded());
214
215        View origView1 = f1.getView();
216        assertNotNull("fragment 1 returned null view", origView1);
217        assertTrue("fragment 1's view not attached", origView1.isAttachedToWindow());
218
219        fm.beginTransaction().replace(android.R.id.content, f2).addToBackStack("stack1").commit();
220        executePendingTransactions(fm);
221
222        assertFalse("fragment 1 is added", f1.isAdded());
223        assertTrue("fragment 2 is added", f2.isAdded());
224        assertNull("fragment 1 returned non-null view", f1.getView());
225        assertFalse("fragment 1's old view still attached", origView1.isAttachedToWindow());
226        View origView2 = f2.getView();
227        assertNotNull("fragment 2 returned null view", origView2);
228        assertTrue("fragment 2's view not attached", origView2.isAttachedToWindow());
229
230        fm.popBackStack();
231        executePendingTransactions(fm);
232
233        assertTrue("fragment 1 is not added", f1.isAdded());
234        assertFalse("fragment 2 is added", f2.isAdded());
235        assertNull("fragment 2 returned non-null view", f2.getView());
236        assertFalse("fragment 2's view still attached", origView2.isAttachedToWindow());
237        View newView1 = f1.getView();
238        assertNotSame("fragment 1 had same view from last attachment", origView1, newView1);
239        assertTrue("fragment 1's view not attached", newView1.isAttachedToWindow());
240    }
241
242    @Test
243    @UiThreadTest
244    public void restoreRetainedInstanceFragments() throws Throwable {
245        // Create a new FragmentManager in isolation, nest some assorted fragments
246        // and then restore them to a second new FragmentManager.
247
248        final FragmentController fc1 = FragmentController.createController(
249                new HostCallbacks(mActivityRule.getActivity()));
250
251        final FragmentManager fm1 = fc1.getSupportFragmentManager();
252
253        fc1.attachHost(null);
254        fc1.dispatchCreate();
255
256        // Configure fragments.
257
258        // Grandparent fragment will not retain instance
259        final StateSaveFragment grandparentFragment = new StateSaveFragment("Grandparent",
260                "UnsavedGrandparent");
261        assertNotNull("grandparent fragment saved state not initialized",
262                grandparentFragment.getSavedState());
263        assertNotNull("grandparent fragment unsaved state not initialized",
264                grandparentFragment.getUnsavedState());
265        fm1.beginTransaction().add(grandparentFragment, "tag:grandparent").commitNow();
266
267        // Parent fragment will retain instance
268        final StateSaveFragment parentFragment = new StateSaveFragment("Parent", "UnsavedParent");
269        assertNotNull("parent fragment saved state not initialized",
270                parentFragment.getSavedState());
271        assertNotNull("parent fragment unsaved state not initialized",
272                parentFragment.getUnsavedState());
273        parentFragment.setRetainInstance(true);
274        grandparentFragment.getChildFragmentManager().beginTransaction()
275                .add(parentFragment, "tag:parent").commitNow();
276        assertSame("parent fragment is not a child of grandparent",
277                grandparentFragment, parentFragment.getParentFragment());
278
279        // Child fragment will not retain instance
280        final StateSaveFragment childFragment = new StateSaveFragment("Child", "UnsavedChild");
281        assertNotNull("child fragment saved state not initialized",
282                childFragment.getSavedState());
283        assertNotNull("child fragment unsaved state not initialized",
284                childFragment.getUnsavedState());
285        parentFragment.getChildFragmentManager().beginTransaction()
286                .add(childFragment, "tag:child").commitNow();
287        assertSame("child fragment is not a child of grandpanret",
288                parentFragment, childFragment.getParentFragment());
289
290        // Saved for comparison later
291        final FragmentManager parentChildFragmentManager = parentFragment.getChildFragmentManager();
292
293        fc1.dispatchActivityCreated();
294        fc1.noteStateNotSaved();
295        fc1.execPendingActions();
296        fc1.doLoaderStart();
297        fc1.dispatchStart();
298        fc1.reportLoaderStart();
299        fc1.dispatchResume();
300        fc1.execPendingActions();
301
302        // Bring the state back down to destroyed, simulating an activity restart
303        fc1.dispatchPause();
304        final Parcelable savedState = fc1.saveAllState();
305        final FragmentManagerNonConfig nonconf = fc1.retainNestedNonConfig();
306        fc1.dispatchStop();
307        fc1.dispatchReallyStop();
308        fc1.dispatchDestroy();
309
310        // Create the new controller and restore state
311        final FragmentController fc2 = FragmentController.createController(
312                new HostCallbacks(mActivityRule.getActivity()));
313
314        final FragmentManager fm2 = fc2.getSupportFragmentManager();
315
316        fc2.attachHost(null);
317        fc2.restoreAllState(savedState, nonconf);
318        fc2.dispatchCreate();
319
320        // Confirm that the restored fragments are available and in the expected states
321        final StateSaveFragment restoredGrandparent = (StateSaveFragment) fm2.findFragmentByTag(
322                "tag:grandparent");
323        assertNotNull("grandparent fragment not restored", restoredGrandparent);
324
325        assertNotSame("grandparent fragment instance was saved",
326                grandparentFragment, restoredGrandparent);
327        assertEquals("grandparent fragment saved state was not equal",
328                grandparentFragment.getSavedState(), restoredGrandparent.getSavedState());
329        assertNotEquals("grandparent fragment unsaved state was unexpectedly preserved",
330                grandparentFragment.getUnsavedState(), restoredGrandparent.getUnsavedState());
331
332        final StateSaveFragment restoredParent = (StateSaveFragment) restoredGrandparent
333                .getChildFragmentManager().findFragmentByTag("tag:parent");
334        assertNotNull("parent fragment not restored", restoredParent);
335
336        assertSame("parent fragment instance was not saved", parentFragment, restoredParent);
337        assertEquals("parent fragment saved state was not equal",
338                parentFragment.getSavedState(), restoredParent.getSavedState());
339        assertEquals("parent fragment unsaved state was not equal",
340                parentFragment.getUnsavedState(), restoredParent.getUnsavedState());
341        assertNotSame("parent fragment has the same child FragmentManager",
342                parentChildFragmentManager, restoredParent.getChildFragmentManager());
343
344        final StateSaveFragment restoredChild = (StateSaveFragment) restoredParent
345                .getChildFragmentManager().findFragmentByTag("tag:child");
346        assertNotNull("child fragment not restored", restoredChild);
347
348        assertNotSame("child fragment instance state was saved", childFragment, restoredChild);
349        assertEquals("child fragment saved state was not equal",
350                childFragment.getSavedState(), restoredChild.getSavedState());
351        assertNotEquals("child fragment saved state was unexpectedly equal",
352                childFragment.getUnsavedState(), restoredChild.getUnsavedState());
353
354        fc2.dispatchActivityCreated();
355        fc2.noteStateNotSaved();
356        fc2.execPendingActions();
357        fc2.doLoaderStart();
358        fc2.dispatchStart();
359        fc2.reportLoaderStart();
360        fc2.dispatchResume();
361        fc2.execPendingActions();
362
363        // Test that the fragments are in the configuration we expect
364
365        // Bring the state back down to destroyed before we finish the test
366        fc2.dispatchPause();
367        fc2.saveAllState();
368        fc2.dispatchStop();
369        fc2.dispatchReallyStop();
370        fc2.dispatchDestroy();
371
372        assertTrue("grandparent not destroyed", restoredGrandparent.mCalledOnDestroy);
373        assertTrue("parent not destroyed", restoredParent.mCalledOnDestroy);
374        assertTrue("child not destroyed", restoredChild.mCalledOnDestroy);
375    }
376
377    @Test
378    @UiThreadTest
379    public void saveAnimationState() throws Throwable {
380        FragmentController fc = startupFragmentController(null);
381        FragmentManager fm = fc.getSupportFragmentManager();
382
383        fm.beginTransaction()
384                .setCustomAnimations(0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
385                .add(android.R.id.content, SimpleFragment.create(R.layout.fragment_a))
386                .addToBackStack(null)
387                .commit();
388        fm.executePendingTransactions();
389
390        assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
391
392        // Causes save and restore of fragments and back stack
393        fc = restartFragmentController(fc);
394        fm = fc.getSupportFragmentManager();
395
396        assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
397
398        fm.beginTransaction()
399                .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, 0, 0)
400                .replace(android.R.id.content, SimpleFragment.create(R.layout.fragment_b))
401                .addToBackStack(null)
402                .commit();
403        fm.executePendingTransactions();
404
405        assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
406
407        // Causes save and restore of fragments and back stack
408        fc = restartFragmentController(fc);
409        fm = fc.getSupportFragmentManager();
410
411        assertAnimationsMatch(fm, R.anim.fade_in, R.anim.fade_out, 0, 0);
412
413        fm.popBackStackImmediate();
414
415        assertAnimationsMatch(fm, 0, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out);
416
417        shutdownFragmentController(fc);
418    }
419
420    private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
421            int popExit) {
422        FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
423        BackStackRecord record = fmImpl.mBackStack.get(fmImpl.mBackStack.size() - 1);
424
425        Assert.assertEquals(enter, record.mEnterAnim);
426        Assert.assertEquals(exit, record.mExitAnim);
427        Assert.assertEquals(popEnter, record.mPopEnterAnim);
428        Assert.assertEquals(popExit, record.mPopExitAnim);
429    }
430
431    private FragmentController restartFragmentController(FragmentController fc) {
432        Parcelable savedState = shutdownFragmentController(fc);
433        return startupFragmentController(savedState);
434    }
435
436    private FragmentController startupFragmentController(Parcelable savedState) {
437        final FragmentController fc = FragmentController.createController(
438                new HostCallbacks(mActivityRule.getActivity()));
439        fc.attachHost(null);
440        fc.restoreAllState(savedState, (FragmentManagerNonConfig) null);
441        fc.dispatchCreate();
442        fc.dispatchActivityCreated();
443        fc.noteStateNotSaved();
444        fc.execPendingActions();
445        fc.doLoaderStart();
446        fc.dispatchStart();
447        fc.reportLoaderStart();
448        fc.dispatchResume();
449        fc.execPendingActions();
450        return fc;
451    }
452
453    private Parcelable shutdownFragmentController(FragmentController fc) {
454        fc.dispatchPause();
455        final Parcelable savedState = fc.saveAllState();
456        fc.dispatchStop();
457        fc.dispatchReallyStop();
458        fc.dispatchDestroy();
459        return savedState;
460    }
461
462    private void executePendingTransactions(final FragmentManager fm) throws Throwable {
463        mActivityRule.runOnUiThread(new Runnable() {
464            @Override
465            public void run() {
466                fm.executePendingTransactions();
467            }
468        });
469    }
470
471    public static class StateSaveFragment extends StrictFragment {
472        private static final String STATE_KEY = "state";
473
474        private String mSavedState;
475        private String mUnsavedState;
476
477        public StateSaveFragment() {
478        }
479
480        public StateSaveFragment(String savedState, String unsavedState) {
481            mSavedState = savedState;
482            mUnsavedState = unsavedState;
483        }
484
485        public String getSavedState() {
486            return mSavedState;
487        }
488
489        public String getUnsavedState() {
490            return mUnsavedState;
491        }
492
493        @Override
494        public void onCreate(Bundle savedInstanceState) {
495            super.onCreate(savedInstanceState);
496            if (savedInstanceState != null) {
497                mSavedState = savedInstanceState.getString(STATE_KEY);
498            }
499        }
500
501        @Override
502        public void onSaveInstanceState(Bundle outState) {
503            super.onSaveInstanceState(outState);
504            outState.putString(STATE_KEY, mSavedState);
505        }
506    }
507
508    static class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
509        private final FragmentActivity mActivity;
510
511        public HostCallbacks(FragmentActivity activity) {
512            super(activity);
513            mActivity = activity;
514        }
515
516        @Override
517        public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
518        }
519
520        @Override
521        public boolean onShouldSaveFragmentState(Fragment fragment) {
522            return !mActivity.isFinishing();
523        }
524
525        @Override
526        public LayoutInflater onGetLayoutInflater() {
527            return mActivity.getLayoutInflater().cloneInContext(mActivity);
528        }
529
530        @Override
531        public FragmentActivity onGetHost() {
532            return mActivity;
533        }
534
535        @Override
536        public void onSupportInvalidateOptionsMenu() {
537            mActivity.supportInvalidateOptionsMenu();
538        }
539
540        @Override
541        public void onStartActivityFromFragment(Fragment fragment, Intent intent, int requestCode) {
542            mActivity.startActivityFromFragment(fragment, intent, requestCode);
543        }
544
545        @Override
546        public void onStartActivityFromFragment(
547                Fragment fragment, Intent intent, int requestCode, @Nullable Bundle options) {
548            mActivity.startActivityFromFragment(fragment, intent, requestCode, options);
549        }
550
551        @Override
552        public void onRequestPermissionsFromFragment(@NonNull Fragment fragment,
553                @NonNull String[] permissions, int requestCode) {
554            throw new UnsupportedOperationException();
555        }
556
557        @Override
558        public boolean onShouldShowRequestPermissionRationale(@NonNull String permission) {
559            return ActivityCompat.shouldShowRequestPermissionRationale(
560                    mActivity, permission);
561        }
562
563        @Override
564        public boolean onHasWindowAnimations() {
565            return mActivity.getWindow() != null;
566        }
567
568        @Override
569        public int onGetWindowAnimations() {
570            final Window w = mActivity.getWindow();
571            return (w == null) ? 0 : w.getAttributes().windowAnimations;
572        }
573
574        @Override
575        public void onAttachFragment(Fragment fragment) {
576            mActivity.onAttachFragment(fragment);
577        }
578
579        @Nullable
580        @Override
581        public View onFindViewById(int id) {
582            return mActivity.findViewById(id);
583        }
584
585        @Override
586        public boolean onHasView() {
587            final Window w = mActivity.getWindow();
588            return (w != null && w.peekDecorView() != null);
589        }
590    }
591
592    public static class SimpleFragment extends Fragment {
593        private int mLayoutId;
594        private static final String LAYOUT_ID = "layoutId";
595
596        @Override
597        public void onCreate(Bundle savedInstanceState) {
598            super.onCreate(savedInstanceState);
599            if (savedInstanceState != null) {
600                mLayoutId = savedInstanceState.getInt(LAYOUT_ID, mLayoutId);
601            }
602        }
603
604        @Override
605        public void onSaveInstanceState(Bundle outState) {
606            super.onSaveInstanceState(outState);
607            outState.putInt(LAYOUT_ID, mLayoutId);
608        }
609
610        @Override
611        public View onCreateView(LayoutInflater inflater, ViewGroup container,
612                Bundle savedInstanceState) {
613            return inflater.inflate(mLayoutId, container, false);
614        }
615
616        public static SimpleFragment create(int layoutId) {
617            SimpleFragment fragment = new SimpleFragment();
618            fragment.mLayoutId = layoutId;
619            return fragment;
620        }
621    }
622}
623