• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 com.android.settings;
18 
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.os.Bundle;
22 import android.util.Log;
23 
24 import androidx.annotation.CallSuper;
25 import androidx.annotation.IntDef;
26 
27 import com.android.settingslib.utils.ThreadUtils;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.Locale;
32 import java.util.Set;
33 import java.util.concurrent.CopyOnWriteArraySet;
34 
35 /**
36  * A headless fragment encapsulating a long-running action such as a network RPC surviving rotation.
37  *
38  * <p>Subclasses should implement their own state machine, updating the state on each state change
39  * via {@link #setState(int, int)}. They can define their own states, however, it is suggested that
40  * the pre-defined {@link @State} constants are used and customizations are implemented via
41  * substates. Custom states must be outside the range of pre-defined states.
42  *
43  * <p>It is safe to update the state at any time, but state updates must originate from the main
44  * thread.
45  *
46  * <p>A listener can be attached that receives state updates while it's registered. Note that state
47  * change events can occur at any point in time and hence a registered listener should unregister if
48  * it cannot act upon the state change (typically a non-resumed fragment).
49  *
50  * <p>Listeners can receive state changes for the same state/substate combination, so listeners
51  * should make sure to be idempotent during state change events.
52  *
53  * <p>If a SidecarFragment is only relevant during the lifetime of another fragment (for example, a
54  * sidecar performing a details request for a DetailsFragment), that fragment needs to become the
55  * managing fragment of the sidecar.
56  *
57  * <h2>Managing fragment responsibilities</h2>
58  *
59  * <ol>
60  *   <li>Instantiates the sidecar fragment when necessary, preferably in {@link #onStart}.
61  *   <li>Removes the sidecar fragment when it's no longer used or when itself is removed. Removal of
62  *       the managing fragment can be detected by checking {@link #isRemoving} in {@link #onStop}.
63  *       <br>
64  *   <li>Registers as a listener in {@link #onResume()}, unregisters in {@link #onPause()}.
65  *   <li>Starts the long-running operation by calling into the sidecar.
66  *   <li>Receives state updates via {@link Listener#onStateChange(SidecarFragment)} and updates the
67  *       UI accordingly.
68  * </ol>
69  *
70  * <h2>Managing fragment example</h2>
71  *
72  * <pre>
73  *     public class MainFragment implements SidecarFragment.Listener {
74  *         private static final String TAG_SOME_SIDECAR = ...;
75  *         private static final String KEY_SOME_SIDECAR_STATE = ...;
76  *
77  *         private SomeSidecarFragment mSidecar;
78  *
79  *         &#064;Override
80  *         public void onStart() {
81  *             super.onStart();
82  *             Bundle args = ...; // optional args
83  *             mSidecar = SidecarFragment.get(getFragmentManager(), TAG_SOME_SIDECAR,
84  *                     SidecarFragment.class, args);
85  *         }
86  *
87  *         &#064;Override
88  *         public void onResume() {
89  *             mSomeSidecar.addListener(this);
90  *         }
91  *
92  *         &#064;Override
93  *         public void onPause() {
94  *             mSomeSidecar.removeListener(this):
95  *         }
96  *     }
97  * </pre>
98  */
99 public class SidecarFragment extends Fragment {
100 
101     private static final String TAG = "SidecarFragment";
102 
103     /**
104      * Get an instance of this sidecar.
105      *
106      * <p>Will return the existing instance if one is already present. Note that the args will not
107      * be used in this situation, so args must be constant for any particular fragment manager and
108      * tag.
109      */
110     @SuppressWarnings("unchecked")
get( FragmentManager fm, String tag, Class<T> clazz, Bundle args)111     protected static <T extends SidecarFragment> T get(
112             FragmentManager fm, String tag, Class<T> clazz, Bundle args) {
113         T fragment = (T) fm.findFragmentByTag(tag);
114         if (fragment == null) {
115             try {
116                 fragment = clazz.newInstance();
117             } catch (java.lang.InstantiationException e) {
118                 throw new InstantiationException("Unable to create fragment", e);
119             } catch (IllegalAccessException e) {
120                 throw new IllegalArgumentException("Unable to create fragment", e);
121             }
122             if (args != null) {
123                 fragment.setArguments(args);
124             }
125             fm.beginTransaction().add(fragment, tag).commit();
126             // No real harm in doing this here - get() should generally only be called from onCreate
127             // which is on the main thread - and it allows us to start running the sidecar on this
128             // instance immediately rather than having to wait until the transaction commits.
129             fm.executePendingTransactions();
130         }
131 
132         return fragment;
133     }
134 
135     /** State definitions. @see {@link #getState} */
136     @Retention(RetentionPolicy.SOURCE)
137     @IntDef({State.INIT, State.RUNNING, State.SUCCESS, State.ERROR})
138     public @interface State {
139         /** Initial idling state. */
140         int INIT = 0;
141 
142         /** The long-running operation is in progress. */
143         int RUNNING = 1;
144 
145         /** The long-running operation has succeeded. */
146         int SUCCESS = 2;
147 
148         /** The long-running operation has failed. */
149         int ERROR = 3;
150     }
151 
152     /** Substate definitions. @see {@link #getSubstate} */
153     @Retention(RetentionPolicy.SOURCE)
154     @IntDef({
155         Substate.UNUSED,
156         Substate.RUNNING_BIND_SERVICE,
157         Substate.RUNNING_GET_ACTIVATION_CODE,
158     })
159     public @interface Substate {
160         // Unknown/unused substate.
161         int UNUSED = 0;
162         int RUNNING_BIND_SERVICE = 1;
163         int RUNNING_GET_ACTIVATION_CODE = 2;
164 
165         // Future tags: 3+
166     }
167 
168     /** **************************************** */
169     private Set<Listener> mListeners = new CopyOnWriteArraySet<>();
170 
171     // Used to track whether onCreate has been called yet.
172     private boolean mCreated;
173 
174     @State private int mState;
175     @Substate private int mSubstate;
176 
177     /** A listener receiving state change events. */
178     public interface Listener {
179 
180         /**
181          * Called upon any state or substate change.
182          *
183          * <p>The new state can be queried through {@link #getState} and {@link #getSubstate}.
184          *
185          * <p>Called from the main thread.
186          *
187          * @param fragment the SidecarFragment that changed its state
188          */
onStateChange(SidecarFragment fragment)189         void onStateChange(SidecarFragment fragment);
190     }
191 
192     @Override
onCreate(Bundle savedInstanceState)193     public void onCreate(Bundle savedInstanceState) {
194         super.onCreate(savedInstanceState);
195         setRetainInstance(true);
196         mCreated = true;
197         setState(State.INIT, Substate.UNUSED);
198     }
199 
200     @Override
onDestroy()201     public void onDestroy() {
202         mCreated = false;
203         super.onDestroy();
204     }
205 
206     /**
207      * Registers a listener that will receive subsequent state changes.
208      *
209      * <p>A {@link Listener#onStateChange(SidecarFragment)} event is fired as part of this call
210      * unless {@link #onCreate} has not yet been called (which means that it's unsafe to access this
211      * fragment as it has not been setup or restored completely). In that case, the future call to
212      * onCreate will trigger onStateChange on registered listener.
213      *
214      * <p>Must be called from the main thread.
215      *
216      * @param listener a listener, or null for unregistering the current listener
217      */
addListener(Listener listener)218     public void addListener(Listener listener) {
219         ThreadUtils.ensureMainThread();
220         mListeners.add(listener);
221         if (mCreated) {
222             notifyListener(listener);
223         }
224     }
225 
226     /**
227      * Removes a previously registered listener.
228      *
229      * @return {@code true} if the listener was removed, {@code false} if there was no such listener
230      *     registered.
231      */
removeListener(Listener listener)232     public boolean removeListener(Listener listener) {
233         ThreadUtils.ensureMainThread();
234         return mListeners.remove(listener);
235     }
236 
237     /** Returns the current state. */
238     @State
getState()239     public int getState() {
240         return mState;
241     }
242 
243     /** Returns the current substate. */
244     @Substate
getSubstate()245     public int getSubstate() {
246         return mSubstate;
247     }
248 
249     /**
250      * Resets the sidecar to its initial state.
251      *
252      * <p>Implementers can override this method to perform additional reset tasks, but must call the
253      * super method.
254      */
255     @CallSuper
reset()256     public void reset() {
257         setState(State.INIT, Substate.UNUSED);
258     }
259 
260     /**
261      * Updates the state and substate and notifies the registered listener.
262      *
263      * <p>Must be called from the main thread.
264      *
265      * @param state the state to transition to
266      * @param substate the substate to transition to
267      */
setState(@tate int state, @Substate int substate)268     protected void setState(@State int state, @Substate int substate) {
269         ThreadUtils.ensureMainThread();
270 
271         mState = state;
272         mSubstate = substate;
273         notifyAllListeners();
274         printState();
275     }
276 
notifyAllListeners()277     private void notifyAllListeners() {
278         for (Listener listener : mListeners) {
279             notifyListener(listener);
280         }
281     }
282 
notifyListener(Listener listener)283     private void notifyListener(Listener listener) {
284         listener.onStateChange(this);
285     }
286 
287     /** Prints the state of the sidecar. */
printState()288     public void printState() {
289         StringBuilder sb =
290                 new StringBuilder("SidecarFragment.setState(): Sidecar Class: ")
291                         .append(getClass().getCanonicalName());
292         sb.append(", State: ");
293         switch (mState) {
294             case SidecarFragment.State.INIT:
295                 sb.append("State.INIT");
296                 break;
297             case SidecarFragment.State.RUNNING:
298                 sb.append("State.RUNNING");
299                 break;
300             case SidecarFragment.State.SUCCESS:
301                 sb.append("State.SUCCESS");
302                 break;
303             case SidecarFragment.State.ERROR:
304                 sb.append("State.ERROR");
305                 break;
306             default:
307                 sb.append(mState);
308                 break;
309         }
310         switch (mSubstate) {
311             case SidecarFragment.Substate.UNUSED:
312                 sb.append(", Substate.UNUSED");
313                 break;
314             default:
315                 sb.append(", ").append(mSubstate);
316                 break;
317         }
318 
319         Log.v(TAG, sb.toString());
320     }
321 
322     @Override
toString()323     public String toString() {
324         return String.format(
325                 Locale.US,
326                 "SidecarFragment[mState=%d, mSubstate=%d]: %s",
327                 mState,
328                 mSubstate,
329                 super.toString());
330     }
331 
332     /** The State of the sidecar status. */
333     public static final class States {
334         public static final States SUCCESS = States.create(State.SUCCESS, Substate.UNUSED);
335         public static final States ERROR = States.create(State.ERROR, Substate.UNUSED);
336 
337         @State public final int state;
338         @Substate public final int substate;
339 
340         /** Creates a new sidecar state. */
create(@tate int state, @Substate int substate)341         public static States create(@State int state, @Substate int substate) {
342             return new States(state, substate);
343         }
344 
States(@tate int state, @Substate int substate)345         public States(@State int state, @Substate int substate) {
346             this.state = state;
347             this.substate = substate;
348         }
349 
350         @Override
equals(Object o)351         public boolean equals(Object o) {
352             if (!(o instanceof States)) {
353                 return false;
354             }
355             States other = (States) o;
356             return this.state == other.state && this.substate == other.substate;
357         }
358 
359         @Override
hashCode()360         public int hashCode() {
361             return state * 31 + substate;
362         }
363     }
364 }
365