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 package androidx.fragment.app
18 
19 import android.content.Context
20 import android.os.Bundle
21 import android.util.AttributeSet
22 import androidx.annotation.LayoutRes
23 
24 /**
25  * This fragment watches its primary lifecycle events and throws IllegalStateException if any of
26  * them are called out of order or from a bad/unexpected state.
27  */
28 open class StrictFragment(@LayoutRes contentLayoutId: Int = 0) : Fragment(contentLayoutId) {
29     var currentState: State = State.DETACHED
30 
31     var calledOnAttach: Boolean = false
32     var calledOnCreate: Boolean = false
33     var calledOnActivityCreated: Boolean = false
34     var calledOnStart: Boolean = false
35     var calledOnResume: Boolean = false
36     var calledOnSaveInstanceState: Boolean = false
37     var calledOnPause: Boolean = false
38     var calledOnStop: Boolean = false
39     var calledOnDestroy: Boolean = false
40     var calledOnDetach: Boolean = false
41     var calledOnAttachFragment: Boolean = false
42     var lastSavedInstanceState: Bundle? = null
43 
onStateChangednull44     open fun onStateChanged(fromState: State) {
45         checkGetActivity()
46     }
47 
checkGetActivitynull48     fun checkGetActivity() {
49         checkNotNull(activity) { "getActivity() returned null at unexpected time" }
50     }
51 
checkActivityNotDestroyednull52     fun checkActivityNotDestroyed() {
53         check(!requireActivity().isDestroyed)
54     }
55 
checkStatenull56     fun checkState(caller: String, vararg expected: State) {
57         if (expected.isEmpty()) {
58             throw IllegalArgumentException("must supply at least one expected state")
59         }
60         for (expect in expected) {
61             if (currentState == expect) {
62                 return
63             }
64         }
65         val expectString = StringBuilder(expected[0].toString())
66         for (i in 1 until expected.size) {
67             expectString.append(" or ").append(expected[i])
68         }
69         throw IllegalStateException(
70             "$caller called while fragment was $currentState; " + "expected $expectString"
71         )
72     }
73 
checkStateAtLeastnull74     fun checkStateAtLeast(caller: String, minState: State) {
75         if (currentState < minState) {
76             throw IllegalStateException(
77                 "$caller called while fragment was $currentState; " + "expected at least $minState"
78             )
79         }
80     }
81 
82     @Deprecated("Deprecated in superclass")
onAttachFragmentnull83     override fun onAttachFragment(childFragment: Fragment) {
84         calledOnAttachFragment = true
85     }
86 
onInflatenull87     override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) {
88         super.onInflate(context, attrs, savedInstanceState)
89         checkActivityNotDestroyed()
90         checkState("onInflate", State.DETACHED)
91     }
92 
onAttachnull93     override fun onAttach(context: Context) {
94         super.onAttach(context)
95         checkActivityNotDestroyed()
96         calledOnAttach = true
97         checkState("onAttach", State.DETACHED)
98         currentState = State.ATTACHED
99         onStateChanged(State.DETACHED)
100         @Suppress("Deprecation") // We're not setting retainInstance, just supporting it
101         if (retainInstance && calledOnCreate) {
102             // We were created previously
103             currentState = State.CREATED
104             onStateChanged(State.ATTACHED)
105         }
106     }
107 
onCreatenull108     override fun onCreate(savedInstanceState: Bundle?) {
109         super.onCreate(savedInstanceState)
110         checkActivityNotDestroyed()
111         if (calledOnCreate && !calledOnDestroy) {
112             throw IllegalStateException("onCreate called more than once with no onDestroy")
113         }
114         calledOnCreate = true
115         lastSavedInstanceState = savedInstanceState
116         checkState("onCreate", State.ATTACHED)
117         currentState = State.CREATED
118         onStateChanged(State.ATTACHED)
119     }
120 
121     @Deprecated("Deprecated in superclass")
onActivityCreatednull122     override fun onActivityCreated(savedInstanceState: Bundle?) {
123         @Suppress("Deprecation") // we're just calling the superclass method with the same name
124         super.onActivityCreated(savedInstanceState)
125         checkActivityNotDestroyed()
126         calledOnActivityCreated = true
127         checkState("onActivityCreated", State.ATTACHED, State.CREATED, State.VIEW_CREATED)
128         val fromState = currentState
129         currentState = State.ACTIVITY_CREATED
130         onStateChanged(fromState)
131     }
132 
onStartnull133     override fun onStart() {
134         super.onStart()
135         checkActivityNotDestroyed()
136         calledOnStart = true
137         checkState("onStart", State.CREATED, State.ACTIVITY_CREATED)
138         currentState = State.STARTED
139         onStateChanged(State.ACTIVITY_CREATED)
140     }
141 
onResumenull142     override fun onResume() {
143         super.onResume()
144         checkActivityNotDestroyed()
145         calledOnResume = true
146         checkState("onResume", State.STARTED)
147         currentState = State.RESUMED
148         onStateChanged(State.STARTED)
149     }
150 
onSaveInstanceStatenull151     override fun onSaveInstanceState(outState: Bundle) {
152         super.onSaveInstanceState(outState)
153         calledOnSaveInstanceState = true
154         checkGetActivity()
155         // FIXME: We should not allow onSaveInstanceState except when STARTED or greater.
156         // But FragmentManager currently does it in saveAllState for fragments on the
157         // back stack, so fragments may be in the CREATED state.
158         checkStateAtLeast("onSaveInstanceState", State.CREATED)
159     }
160 
onPausenull161     override fun onPause() {
162         super.onPause()
163         calledOnPause = true
164         checkState("onPause", State.RESUMED)
165         currentState = State.STARTED
166         onStateChanged(State.RESUMED)
167     }
168 
onStopnull169     override fun onStop() {
170         super.onStop()
171         calledOnStop = true
172         checkState("onStop", State.STARTED)
173         currentState = State.CREATED
174         onStateChanged(State.STARTED)
175     }
176 
onDestroynull177     override fun onDestroy() {
178         super.onDestroy()
179         calledOnDestroy = true
180         checkState("onDestroy", State.CREATED)
181         currentState = State.ATTACHED
182         onStateChanged(State.CREATED)
183     }
184 
onDetachnull185     override fun onDetach() {
186         super.onDetach()
187         calledOnDetach = true
188         checkState("onDestroy", State.CREATED, State.ATTACHED)
189         val fromState = currentState
190         currentState = State.DETACHED
191         onStateChanged(fromState)
192     }
193 
194     enum class State {
195         DETACHED,
196         ATTACHED,
197         CREATED,
198         VIEW_CREATED,
199         ACTIVITY_CREATED,
200         STARTED,
201         RESUMED
202     }
203 }
204