blob: 1a69c03f48dbdbbf2be87a009aa1d6eec35adba4 [file] [log] [blame]
Jiashen Wangd8bd3bb2020-09-08 15:57:40 -07001/*
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
17package com.android.settings;
18
19import android.app.Fragment;
20import android.app.FragmentManager;
21import android.os.Bundle;
22import android.util.Log;
23
24import androidx.annotation.CallSuper;
25import androidx.annotation.IntDef;
26
27import com.android.settingslib.utils.ThreadUtils;
28
29import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.util.Locale;
32import java.util.Set;
33import 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 */
99public 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")
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 */
189 void onStateChange(SidecarFragment fragment);
190 }
191
192 @Override
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
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 */
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 */
232 public boolean removeListener(Listener listener) {
233 ThreadUtils.ensureMainThread();
234 return mListeners.remove(listener);
235 }
236
237 /** Returns the current state. */
238 @State
239 public int getState() {
240 return mState;
241 }
242
243 /** Returns the current substate. */
244 @Substate
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
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 */
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
277 private void notifyAllListeners() {
278 for (Listener listener : mListeners) {
279 notifyListener(listener);
280 }
281 }
282
283 private void notifyListener(Listener listener) {
284 listener.onStateChange(this);
285 }
286
287 /** Prints the state of the sidecar. */
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
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. */
341 public static States create(@State int state, @Substate int substate) {
342 return new States(state, substate);
343 }
344
345 public States(@State int state, @Substate int substate) {
346 this.state = state;
347 this.substate = substate;
348 }
349
350 @Override
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
360 public int hashCode() {
361 return state * 31 + substate;
362 }
363 }
364}