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  */
16 
17 
18 package androidx.fragment.app;
19 
20 import android.content.Context;
21 import android.os.Bundle;
22 
23 /**
24  * This fragment watches its primary lifecycle events and throws IllegalStateException
25  * if any of them are called out of order or from a bad/unexpected state.
26  */
27 public class StrictFragment extends Fragment {
28     public static final int DETACHED = 0;
29     public static final int ATTACHED = 1;
30     public static final int CREATED = 2;
31     public static final int ACTIVITY_CREATED = 3;
32     public static final int STARTED = 4;
33     public static final int RESUMED = 5;
34 
35     int mState;
36 
37     boolean mCalledOnAttach, mCalledOnCreate, mCalledOnActivityCreated,
38             mCalledOnStart, mCalledOnResume, mCalledOnSaveInstanceState,
39             mCalledOnPause, mCalledOnStop, mCalledOnDestroy, mCalledOnDetach,
40             mCalledOnAttachFragment;
41 
stateToString(int state)42     static String stateToString(int state) {
43         switch (state) {
44             case DETACHED: return "DETACHED";
45             case ATTACHED: return "ATTACHED";
46             case CREATED: return "CREATED";
47             case ACTIVITY_CREATED: return "ACTIVITY_CREATED";
48             case STARTED: return "STARTED";
49             case RESUMED: return "RESUMED";
50         }
51         return "(unknown " + state + ")";
52     }
53 
onStateChanged(int fromState)54     public void onStateChanged(int fromState) {
55         checkGetActivity();
56     }
57 
checkGetActivity()58     public void checkGetActivity() {
59         if (getActivity() == null) {
60             throw new IllegalStateException("getActivity() returned null at unexpected time");
61         }
62     }
63 
checkState(String caller, int... expected)64     public void checkState(String caller, int... expected) {
65         if (expected == null || expected.length == 0) {
66             throw new IllegalArgumentException("must supply at least one expected state");
67         }
68         for (int expect : expected) {
69             if (mState == expect) {
70                 return;
71             }
72         }
73         final StringBuilder expectString = new StringBuilder(stateToString(expected[0]));
74         for (int i = 1; i < expected.length; i++) {
75             expectString.append(" or ").append(stateToString(expected[i]));
76         }
77         throw new IllegalStateException(caller + " called while fragment was "
78                 + stateToString(mState) + "; expected " + expectString.toString());
79     }
80 
checkStateAtLeast(String caller, int minState)81     public void checkStateAtLeast(String caller, int minState) {
82         if (mState < minState) {
83             throw new IllegalStateException(caller + " called while fragment was "
84                     + stateToString(mState) + "; expected at least " + stateToString(minState));
85         }
86     }
87 
88     @Override
onAttachFragment(Fragment childFragment)89     public void onAttachFragment(Fragment childFragment) {
90         mCalledOnAttachFragment = true;
91     }
92 
93     @Override
onAttach(Context context)94     public void onAttach(Context context) {
95         super.onAttach(context);
96         mCalledOnAttach = true;
97         checkState("onAttach", DETACHED);
98         mState = ATTACHED;
99         onStateChanged(DETACHED);
100     }
101 
102     @Override
onCreate(Bundle savedInstanceState)103     public void onCreate(Bundle savedInstanceState) {
104         super.onCreate(savedInstanceState);
105         if (mCalledOnCreate && !mCalledOnDestroy) {
106             throw new IllegalStateException("onCreate called more than once with no onDestroy");
107         }
108         mCalledOnCreate = true;
109         checkState("onCreate", ATTACHED);
110         mState = CREATED;
111         onStateChanged(ATTACHED);
112     }
113 
114     @Override
onActivityCreated(Bundle savedInstanceState)115     public void onActivityCreated(Bundle savedInstanceState) {
116         super.onActivityCreated(savedInstanceState);
117         mCalledOnActivityCreated = true;
118         checkState("onActivityCreated", ATTACHED, CREATED);
119         int fromState = mState;
120         mState = ACTIVITY_CREATED;
121         onStateChanged(fromState);
122     }
123 
124     @Override
onStart()125     public void onStart() {
126         super.onStart();
127         mCalledOnStart = true;
128         checkState("onStart", ACTIVITY_CREATED);
129         mState = STARTED;
130         onStateChanged(ACTIVITY_CREATED);
131     }
132 
133     @Override
onResume()134     public void onResume() {
135         super.onResume();
136         mCalledOnResume = true;
137         checkState("onResume", STARTED);
138         mState = RESUMED;
139         onStateChanged(STARTED);
140     }
141 
142     @Override
onSaveInstanceState(Bundle outState)143     public void onSaveInstanceState(Bundle outState) {
144         super.onSaveInstanceState(outState);
145         mCalledOnSaveInstanceState = true;
146         checkGetActivity();
147         // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
148         // But FragmentManager currently does it in saveAllState for fragments on the
149         // back stack, so fragments may be in the CREATED state.
150         checkStateAtLeast("onSaveInstanceState", CREATED);
151     }
152 
153     @Override
onPause()154     public void onPause() {
155         super.onPause();
156         mCalledOnPause = true;
157         checkState("onPause", RESUMED);
158         mState = STARTED;
159         onStateChanged(RESUMED);
160     }
161 
162     @Override
onStop()163     public void onStop() {
164         super.onStop();
165         mCalledOnStop = true;
166         checkState("onStop", STARTED);
167         mState = CREATED;
168         onStateChanged(STARTED);
169     }
170 
171     @Override
onDestroy()172     public void onDestroy() {
173         super.onDestroy();
174         mCalledOnDestroy = true;
175         checkState("onDestroy", CREATED);
176         mState = ATTACHED;
177         onStateChanged(CREATED);
178     }
179 
180     @Override
onDetach()181     public void onDetach() {
182         super.onDetach();
183         mCalledOnDetach = true;
184         checkState("onDestroy", CREATED, ATTACHED);
185         int fromState = mState;
186         mState = DETACHED;
187         onStateChanged(fromState);
188     }
189 }
190