/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings; import android.app.Fragment; import android.app.FragmentManager; import android.os.Bundle; import android.util.Log; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import com.android.settingslib.utils.ThreadUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Locale; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; /** * A headless fragment encapsulating a long-running action such as a network RPC surviving rotation. * *

Subclasses should implement their own state machine, updating the state on each state change * via {@link #setState(int, int)}. They can define their own states, however, it is suggested that * the pre-defined {@link @State} constants are used and customizations are implemented via * substates. Custom states must be outside the range of pre-defined states. * *

It is safe to update the state at any time, but state updates must originate from the main * thread. * *

A listener can be attached that receives state updates while it's registered. Note that state * change events can occur at any point in time and hence a registered listener should unregister if * it cannot act upon the state change (typically a non-resumed fragment). * *

Listeners can receive state changes for the same state/substate combination, so listeners * should make sure to be idempotent during state change events. * *

If a SidecarFragment is only relevant during the lifetime of another fragment (for example, a * sidecar performing a details request for a DetailsFragment), that fragment needs to become the * managing fragment of the sidecar. * *

Managing fragment responsibilities

* *
    *
  1. Instantiates the sidecar fragment when necessary, preferably in {@link #onStart}. *
  2. Removes the sidecar fragment when it's no longer used or when itself is removed. Removal of * the managing fragment can be detected by checking {@link #isRemoving} in {@link #onStop}. *
    *
  3. Registers as a listener in {@link #onResume()}, unregisters in {@link #onPause()}. *
  4. Starts the long-running operation by calling into the sidecar. *
  5. Receives state updates via {@link Listener#onStateChange(SidecarFragment)} and updates the * UI accordingly. *
* *

Managing fragment example

* *
 *     public class MainFragment implements SidecarFragment.Listener {
 *         private static final String TAG_SOME_SIDECAR = ...;
 *         private static final String KEY_SOME_SIDECAR_STATE = ...;
 *
 *         private SomeSidecarFragment mSidecar;
 *
 *         @Override
 *         public void onStart() {
 *             super.onStart();
 *             Bundle args = ...; // optional args
 *             mSidecar = SidecarFragment.get(getFragmentManager(), TAG_SOME_SIDECAR,
 *                     SidecarFragment.class, args);
 *         }
 *
 *         @Override
 *         public void onResume() {
 *             mSomeSidecar.addListener(this);
 *         }
 *
 *         @Override
 *         public void onPause() {
 *             mSomeSidecar.removeListener(this):
 *         }
 *     }
 * 
*/ public class SidecarFragment extends Fragment { private static final String TAG = "SidecarFragment"; /** * Get an instance of this sidecar. * *

Will return the existing instance if one is already present. Note that the args will not * be used in this situation, so args must be constant for any particular fragment manager and * tag. */ @SuppressWarnings("unchecked") protected static T get( FragmentManager fm, String tag, Class clazz, Bundle args) { T fragment = (T) fm.findFragmentByTag(tag); if (fragment == null) { try { fragment = clazz.newInstance(); } catch (java.lang.InstantiationException e) { throw new InstantiationException("Unable to create fragment", e); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Unable to create fragment", e); } if (args != null) { fragment.setArguments(args); } fm.beginTransaction().add(fragment, tag).commit(); // No real harm in doing this here - get() should generally only be called from onCreate // which is on the main thread - and it allows us to start running the sidecar on this // instance immediately rather than having to wait until the transaction commits. fm.executePendingTransactions(); } return fragment; } /** State definitions. @see {@link #getState} */ @Retention(RetentionPolicy.SOURCE) @IntDef({State.INIT, State.RUNNING, State.SUCCESS, State.ERROR}) public @interface State { /** Initial idling state. */ int INIT = 0; /** The long-running operation is in progress. */ int RUNNING = 1; /** The long-running operation has succeeded. */ int SUCCESS = 2; /** The long-running operation has failed. */ int ERROR = 3; } /** Substate definitions. @see {@link #getSubstate} */ @Retention(RetentionPolicy.SOURCE) @IntDef({ Substate.UNUSED, Substate.RUNNING_BIND_SERVICE, Substate.RUNNING_GET_ACTIVATION_CODE, }) public @interface Substate { // Unknown/unused substate. int UNUSED = 0; int RUNNING_BIND_SERVICE = 1; int RUNNING_GET_ACTIVATION_CODE = 2; // Future tags: 3+ } /** **************************************** */ private Set mListeners = new CopyOnWriteArraySet<>(); // Used to track whether onCreate has been called yet. private boolean mCreated; @State private int mState; @Substate private int mSubstate; /** A listener receiving state change events. */ public interface Listener { /** * Called upon any state or substate change. * *

The new state can be queried through {@link #getState} and {@link #getSubstate}. * *

Called from the main thread. * * @param fragment the SidecarFragment that changed its state */ void onStateChange(SidecarFragment fragment); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mCreated = true; setState(State.INIT, Substate.UNUSED); } @Override public void onDestroy() { mCreated = false; super.onDestroy(); } /** * Registers a listener that will receive subsequent state changes. * *

A {@link Listener#onStateChange(SidecarFragment)} event is fired as part of this call * unless {@link #onCreate} has not yet been called (which means that it's unsafe to access this * fragment as it has not been setup or restored completely). In that case, the future call to * onCreate will trigger onStateChange on registered listener. * *

Must be called from the main thread. * * @param listener a listener, or null for unregistering the current listener */ public void addListener(Listener listener) { ThreadUtils.ensureMainThread(); mListeners.add(listener); if (mCreated) { notifyListener(listener); } } /** * Removes a previously registered listener. * * @return {@code true} if the listener was removed, {@code false} if there was no such listener * registered. */ public boolean removeListener(Listener listener) { ThreadUtils.ensureMainThread(); return mListeners.remove(listener); } /** Returns the current state. */ @State public int getState() { return mState; } /** Returns the current substate. */ @Substate public int getSubstate() { return mSubstate; } /** * Resets the sidecar to its initial state. * *

Implementers can override this method to perform additional reset tasks, but must call the * super method. */ @CallSuper public void reset() { setState(State.INIT, Substate.UNUSED); } /** * Updates the state and substate and notifies the registered listener. * *

Must be called from the main thread. * * @param state the state to transition to * @param substate the substate to transition to */ protected void setState(@State int state, @Substate int substate) { ThreadUtils.ensureMainThread(); mState = state; mSubstate = substate; notifyAllListeners(); printState(); } private void notifyAllListeners() { for (Listener listener : mListeners) { notifyListener(listener); } } private void notifyListener(Listener listener) { listener.onStateChange(this); } /** Prints the state of the sidecar. */ public void printState() { StringBuilder sb = new StringBuilder("SidecarFragment.setState(): Sidecar Class: ") .append(getClass().getCanonicalName()); sb.append(", State: "); switch (mState) { case SidecarFragment.State.INIT: sb.append("State.INIT"); break; case SidecarFragment.State.RUNNING: sb.append("State.RUNNING"); break; case SidecarFragment.State.SUCCESS: sb.append("State.SUCCESS"); break; case SidecarFragment.State.ERROR: sb.append("State.ERROR"); break; default: sb.append(mState); break; } switch (mSubstate) { case SidecarFragment.Substate.UNUSED: sb.append(", Substate.UNUSED"); break; default: sb.append(", ").append(mSubstate); break; } Log.v(TAG, sb.toString()); } @Override public String toString() { return String.format( Locale.US, "SidecarFragment[mState=%d, mSubstate=%d]: %s", mState, mSubstate, super.toString()); } /** The State of the sidecar status. */ public static final class States { public static final States SUCCESS = States.create(State.SUCCESS, Substate.UNUSED); public static final States ERROR = States.create(State.ERROR, Substate.UNUSED); @State public final int state; @Substate public final int substate; /** Creates a new sidecar state. */ public static States create(@State int state, @Substate int substate) { return new States(state, substate); } public States(@State int state, @Substate int substate) { this.state = state; this.substate = substate; } @Override public boolean equals(Object o) { if (!(o instanceof States)) { return false; } States other = (States) o; return this.state == other.state && this.substate == other.substate; } @Override public int hashCode() { return state * 31 + substate; } } }