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 * @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 * @Override 88 * public void onResume() { 89 * mSomeSidecar.addListener(this); 90 * } 91 * 92 * @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