blob: 8d4afc0a1b9da6915b3516bdd3fe594e76763ebd [file] [log] [blame]
/*
* Copyright 2018 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 androidx.fragment.app;
import static androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.EXTRA_ACTIVITY_OPTIONS_BUNDLE;
import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST;
import static androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult.EXTRA_INTENT_SENDER_REQUEST;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.activity.BackEventCompat;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.OnBackPressedDispatcher;
import androidx.activity.OnBackPressedDispatcherOwner;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistry;
import androidx.activity.result.ActivityResultRegistryOwner;
import androidx.activity.result.IntentSenderRequest;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.IdRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.core.app.MultiWindowModeChangedInfo;
import androidx.core.app.OnMultiWindowModeChangedProvider;
import androidx.core.app.OnPictureInPictureModeChangedProvider;
import androidx.core.app.PictureInPictureModeChangedInfo;
import androidx.core.content.OnConfigurationChangedProvider;
import androidx.core.content.OnTrimMemoryProvider;
import androidx.core.util.Consumer;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuProvider;
import androidx.fragment.R;
import androidx.fragment.app.strictmode.FragmentStrictMode;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelStore;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.savedstate.SavedStateRegistry;
import androidx.savedstate.SavedStateRegistryOwner;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Static library support version of the framework's {@link android.app.FragmentManager}.
* Used to write apps that run on platforms prior to Android 3.0. When running
* on Android 3.0 or above, this implementation is still used; it does not try
* to switch to the framework's implementation. See the framework {@link FragmentManager}
* documentation for a class overview.
*
* <p>Your activity must derive from {@link FragmentActivity} to use this. From such an activity,
* you can acquire the {@link FragmentManager} by calling
* {@link FragmentActivity#getSupportFragmentManager}.
*/
public abstract class FragmentManager implements FragmentResultOwner {
private static final String SAVED_STATE_KEY = "android:support:fragments";
private static final String FRAGMENT_MANAGER_STATE_KEY = "state";
private static final String RESULT_KEY_PREFIX = "result_";
private static final String FRAGMENT_KEY_PREFIX = "fragment_";
private static boolean DEBUG = false;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static final String TAG = "FragmentManager";
static boolean USE_PREDICTIVE_BACK = true;
/**
* Control whether FragmentManager uses the new state predictive back feature that allows
* seeing the previous Fragment when using gesture back.
* <p>
* This should only be changed <strong>before</strong> any fragment transactions are done
* (i.e., in your <code>Application</code> class or prior to <code>super.onCreate()</code>
* in every activity).
*
* @param enabled Whether predictive back should be enabled.
*/
@PredictiveBackControl
public static void enablePredictiveBack(boolean enabled) {
FragmentManager.USE_PREDICTIVE_BACK = enabled;
}
/**
* Control whether the framework's internal fragment manager debugging
* logs are turned on. If enabled, you will see output in logcat as
* the framework performs fragment operations.
* @deprecated FragmentManager now respects {@link Log#isLoggable(String, int)} for debug
* logging, allowing you to use <code>adb shell setprop log.tag.FragmentManager VERBOSE</code>.
* @see Log#isLoggable(String, int)
*/
@Deprecated
public static void enableDebugLogging(boolean enabled) {
FragmentManager.DEBUG = enabled;
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static boolean isLoggingEnabled(int level) {
return DEBUG || Log.isLoggable(TAG, level);
}
/**
* Flag for {@link #popBackStack(String, int)}
* and {@link #popBackStack(int, int)}: If set, and the name or ID of
* a back stack entry has been supplied, then all matching entries will
* be consumed until one that doesn't match is found or the bottom of
* the stack is reached. Otherwise, all entries up to but not including that entry
* will be removed.
*/
public static final int POP_BACK_STACK_INCLUSIVE = 1;
/**
* Representation of an entry on the fragment back stack, as created
* with {@link FragmentTransaction#addToBackStack(String)
* FragmentTransaction.addToBackStack()}. Entries can later be
* retrieved with {@link FragmentManager#getBackStackEntryAt(int)
* FragmentManager.getBackStackEntryAt()}.
*
* <p>Note that you should never hold on to a BackStackEntry object;
* the identifier as returned by {@link #getId} is the only thing that
* will be persisted across activity instances.
*/
public interface BackStackEntry {
/**
* Return the unique identifier for the entry. This is the only
* representation of the entry that will persist across activity
* instances.
*/
int getId();
/**
* Get the name that was supplied to
* {@link FragmentTransaction#addToBackStack(String)
* FragmentTransaction.addToBackStack(String)} when creating this entry.
*/
@Nullable
String getName();
/**
* Return the full bread crumb title resource identifier for the entry,
* or 0 if it does not have one.
* @deprecated Store breadcrumb titles separately from back stack entries. For example,
* by using an <code>android:label</code> on a fragment in a navigation graph.
*/
@Deprecated
@StringRes
int getBreadCrumbTitleRes();
/**
* Return the short bread crumb title resource identifier for the entry,
* or 0 if it does not have one.
* @deprecated Store breadcrumb short titles separately from back stack entries. For
* example, by using an <code>android:label</code> on a fragment in a navigation graph.
*/
@Deprecated
@StringRes
int getBreadCrumbShortTitleRes();
/**
* Return the full bread crumb title for the entry, or null if it
* does not have one.
* @deprecated Store breadcrumb titles separately from back stack entries. For example,
* * by using an <code>android:label</code> on a fragment in a navigation graph.
*/
@Deprecated
@Nullable
CharSequence getBreadCrumbTitle();
/**
* Return the short bread crumb title for the entry, or null if it
* does not have one.
* @deprecated Store breadcrumb short titles separately from back stack entries. For
* example, by using an <code>android:label</code> on a fragment in a navigation graph.
*/
@Deprecated
@Nullable
CharSequence getBreadCrumbShortTitle();
}
/**
* Interface to watch for changes to the back stack.
*/
public interface OnBackStackChangedListener {
/**
* Called whenever the contents of the back stack change, after the
* fragment manager has moved all of the fragments to their final state.
*
* <p>
* This is the final callback that will be delivered to the listener in all cases except
* for when the predictive back gesture is cancelled. It will be called after
* {@link #onBackStackChangeCommitted(Fragment, boolean)}.
*/
@MainThread
void onBackStackChanged();
/**
* Called whenever the contents of the back stack are starting to be changed, before
* any fragment actually changes its lifecycle state. If this is caused by a forward
* transaction and the given fragment is incoming, it will return <code>false</code> from
* {@link Fragment#isRemoving()}. If this is caused by a pop operation and the given
* fragment is being popped, it will return <code>true</code> from
* {@link Fragment#isRemoving()}.
*
* <p>
* This is the first callback that will be delivered to the listener. If the transaction
* is caused by a predictive back gesture, it will be followed by an
* {@link #onBackStackChangeProgressed(BackEventCompat)} callback otherwise, the next
* callback will be {@link #onBackStackChangeCommitted(Fragment, boolean)}.
*
* @param fragment one of the fragments that is affected by the starting back stack change
* @param pop true, if this callback was triggered by a pop operation, false otherwise
*/
@MainThread
default void onBackStackChangeStarted(@NonNull Fragment fragment, boolean pop) { }
/**
* Called whenever a predictive back gesture is changing the back stack. This will continue
* to be called with an updated {@link BackEventCompat} object until the gesture is either
* cancelled or completed.
*
* <p>
* This is called immediately after {@link #onBackStackChangeStarted(Fragment, boolean)}
* during a predictive back gesture. If the gesture is completed, this will be followed by
* {@link #onBackStackChangeCommitted(Fragment, boolean)} and if it is cancelled, it will be
* followed by {@link #onBackStackChangeCancelled()}.
*
* @param backEventCompat provides the current progress of the active predictive back
* gesture
*/
@MainThread
default void onBackStackChangeProgressed(@NonNull BackEventCompat backEventCompat) { }
/**
* Called whenever the contents of a back stack change is committed. If this is caused by
* a forward transaction and the given fragment is incoming, it will return
* <code>false</code> from {@link Fragment#isRemoving()}. If this is caused by a pop
* operation and the given fragment is being popped, it will return <code>true</code> from
* {@link Fragment#isRemoving()}.
*
* <p>
* This is called immediately after {@link #onBackStackChangeStarted(Fragment, boolean)}
* for a forward transaction or for a non-predictive back pop. If this is caused by a
* predictive back gesture, it will be called after
* {@link #onBackStackChangeProgressed(BackEventCompat)} before the fragment moves to the
* final state.
*
* @param fragment one of the fragments that is affected by the committed back stack change
* @param pop true, if this callback was triggered by a pop operation, false otherwise
*/
@MainThread
default void onBackStackChangeCommitted(@NonNull Fragment fragment, boolean pop) { }
/**
* Called whenever a predictive back gesture is cancelled.
*
* <p>
* This is called immediately after {@link #onBackStackChangeProgressed(BackEventCompat)}
* once a predictive back gesture has been cancelled and will place the FragmentManager back
* into the state before the predictive back gesture was started.
*/
@MainThread
default void onBackStackChangeCancelled() { }
}
/**
* A {@link FragmentResultListener} that is lifecycle aware so that
* the listener can be fired when the lifecycle is {@link Lifecycle.State#STARTED}.
*/
private static class LifecycleAwareResultListener implements FragmentResultListener {
private final Lifecycle mLifecycle;
private final FragmentResultListener mListener;
private final LifecycleEventObserver mObserver;
LifecycleAwareResultListener(@NonNull Lifecycle lifecycle,
@NonNull FragmentResultListener listener,
@NonNull LifecycleEventObserver observer) {
mLifecycle = lifecycle;
mListener = listener;
mObserver = observer;
}
public boolean isAtLeast(Lifecycle.State state) {
return mLifecycle.getCurrentState().isAtLeast(state);
}
@Override
public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
mListener.onFragmentResult(requestKey, result);
}
public void removeObserver() {
mLifecycle.removeObserver(mObserver);
}
}
/**
* Callback interface for listening to fragment state changes that happen
* within a given FragmentManager.
*/
@SuppressWarnings("unused")
public abstract static class FragmentLifecycleCallbacks {
/**
* Called right before the fragment's {@link Fragment#onAttach(Context)} method is called.
* This is a good time to inject any required dependencies or perform other configuration
* for the fragment before any of the fragment's lifecycle methods are invoked.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param context Context that the Fragment is being attached to
*/
public void onFragmentPreAttached(@NonNull FragmentManager fm, @NonNull Fragment f,
@NonNull Context context) {}
/**
* Called after the fragment has been attached to its host. Its host will have had
* <code>onAttachFragment</code> called before this call happens.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param context Context that the Fragment was attached to
*/
public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f,
@NonNull Context context) {}
/**
* Called right before the fragment's {@link Fragment#onCreate(Bundle)} method is called.
* This is a good time to inject any required dependencies or perform other configuration
* for the fragment.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param savedInstanceState Saved instance bundle from a previous instance
*/
public void onFragmentPreCreated(@NonNull FragmentManager fm, @NonNull Fragment f,
@Nullable Bundle savedInstanceState) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onCreate(Bundle)}. This will only happen once for any given
* fragment instance, though the fragment may be attached and detached multiple times.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param savedInstanceState Saved instance bundle from a previous instance
*/
public void onFragmentCreated(@NonNull FragmentManager fm, @NonNull Fragment f,
@Nullable Bundle savedInstanceState) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onActivityCreated(Bundle)}. This will only happen once for any given
* fragment instance, though the fragment may be attached and detached multiple times.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param savedInstanceState Saved instance bundle from a previous instance
*
* @deprecated To get a callback specifically when a Fragment activity's
* {@link android.app.Activity#onCreate(Bundle)} is called, register a
* {@link androidx.lifecycle.LifecycleObserver} on the Activity's {@link Lifecycle} in
* {@link #onFragmentAttached(FragmentManager, Fragment, Context)}, removing it when it
* receives the {@link Lifecycle.State#CREATED} callback.
*/
@Deprecated
public void onFragmentActivityCreated(@NonNull FragmentManager fm, @NonNull Fragment f,
@Nullable Bundle savedInstanceState) {}
/**
* Called after the fragment has returned a non-null view from the FragmentManager's
* request to {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}.
*
* @param fm Host FragmentManager
* @param f Fragment that created and owns the view
* @param v View returned by the fragment
* @param savedInstanceState Saved instance bundle from a previous instance
*/
public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f,
@NonNull View v, @Nullable Bundle savedInstanceState) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onStart()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onResume()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onPause()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onStop()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentStopped(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onSaveInstanceState(Bundle)}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
* @param outState Saved state bundle for the fragment
*/
public void onFragmentSaveInstanceState(@NonNull FragmentManager fm, @NonNull Fragment f,
@NonNull Bundle outState) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onDestroyView()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onDestroy()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {}
/**
* Called after the fragment has returned from the FragmentManager's call to
* {@link Fragment#onDetach()}.
*
* @param fm Host FragmentManager
* @param f Fragment changing state
*/
public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) {}
}
private final ArrayList<OpGenerator> mPendingActions = new ArrayList<>();
private boolean mExecutingActions;
private final FragmentStore mFragmentStore = new FragmentStore();
ArrayList<BackStackRecord> mBackStack = new ArrayList<>();
private ArrayList<Fragment> mCreatedMenus;
private final FragmentLayoutInflaterFactory mLayoutInflaterFactory =
new FragmentLayoutInflaterFactory(this);
private OnBackPressedDispatcher mOnBackPressedDispatcher;
BackStackRecord mTransitioningOp = null;
boolean mBackStarted = false;
private final OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
public void handleOnBackStarted(@NonNull BackEventCompat backEvent) {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG,
"handleOnBackStarted. PREDICTIVE_BACK = " + USE_PREDICTIVE_BACK
+ " fragment manager " + FragmentManager.this
);
}
if (USE_PREDICTIVE_BACK) {
endAnimatingAwayFragments();
prepareBackStackTransition();
}
}
@Override
public void handleOnBackProgressed(@NonNull BackEventCompat backEvent) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG,
"handleOnBackProgressed. PREDICTIVE_BACK = " + USE_PREDICTIVE_BACK
+ " fragment manager " + FragmentManager.this
);
}
if (mTransitioningOp != null) {
// Collect the correct SpecialEffectsControllers and pass in the progress
Set<SpecialEffectsController> changedControllers =
collectChangedControllers(
new ArrayList<>(
Collections.singletonList(mTransitioningOp)
), 0, 1
);
for (SpecialEffectsController controller: changedControllers) {
controller.processProgress(backEvent);
}
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
listener.onBackStackChangeProgressed(backEvent);
}
}
}
@Override
public void handleOnBackPressed() {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG,
"handleOnBackPressed. PREDICTIVE_BACK = " + USE_PREDICTIVE_BACK
+ " fragment manager " + FragmentManager.this
);
}
FragmentManager.this.handleOnBackPressed();
}
@Override
public void handleOnBackCancelled() {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG,
"handleOnBackCancelled. PREDICTIVE_BACK = " + USE_PREDICTIVE_BACK
+ " fragment manager " + FragmentManager.this
);
}
if (USE_PREDICTIVE_BACK) {
cancelBackStackTransition();
mTransitioningOp = null;
}
}
};
private final AtomicInteger mBackStackIndex = new AtomicInteger();
private final Map<String, BackStackState> mBackStackStates =
Collections.synchronizedMap(new HashMap<String, BackStackState>());
private final Map<String, Bundle> mResults =
Collections.synchronizedMap(new HashMap<String, Bundle>());
private final Map<String, LifecycleAwareResultListener> mResultListeners =
Collections.synchronizedMap(new HashMap<String, LifecycleAwareResultListener>());
ArrayList<OnBackStackChangedListener> mBackStackChangeListeners = new ArrayList<>();
private final FragmentLifecycleCallbacksDispatcher mLifecycleCallbacksDispatcher =
new FragmentLifecycleCallbacksDispatcher(this);
private final CopyOnWriteArrayList<FragmentOnAttachListener> mOnAttachListeners =
new CopyOnWriteArrayList<>();
private final Consumer<Configuration> mOnConfigurationChangedListener = newConfig -> {
if (isParentAdded()) {
dispatchConfigurationChanged(newConfig, false);
}
};
private final Consumer<Integer> mOnTrimMemoryListener = level -> {
if (isParentAdded() && level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
dispatchLowMemory(false);
}
};
private final Consumer<MultiWindowModeChangedInfo> mOnMultiWindowModeChangedListener =
info -> {
if (isParentAdded()) {
dispatchMultiWindowModeChanged(info.isInMultiWindowMode(), false);
}
};
private final Consumer<PictureInPictureModeChangedInfo>
mOnPictureInPictureModeChangedListener = info -> {
if (isParentAdded()) {
dispatchPictureInPictureModeChanged(info.isInPictureInPictureMode(), false);
}
};
private final MenuProvider mMenuProvider = new MenuProvider() {
@Override
public void onPrepareMenu(@NonNull Menu menu) {
dispatchPrepareOptionsMenu(menu);
}
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
dispatchCreateOptionsMenu(menu, menuInflater);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
return dispatchOptionsItemSelected(menuItem);
}
@Override
public void onMenuClosed(@NonNull Menu menu) {
dispatchOptionsMenuClosed(menu);
}
};
int mCurState = Fragment.INITIALIZING;
private FragmentHostCallback<?> mHost;
private FragmentContainer mContainer;
private Fragment mParent;
@SuppressWarnings("WeakerAccess") /* synthetic access */
@Nullable
Fragment mPrimaryNav;
private FragmentFactory mFragmentFactory = null;
private FragmentFactory mHostFragmentFactory = new FragmentFactory() {
@SuppressWarnings("deprecation")
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
return getHost().instantiate(getHost().getContext(), className, null);
}
};
private SpecialEffectsControllerFactory mSpecialEffectsControllerFactory = null;
private SpecialEffectsControllerFactory mDefaultSpecialEffectsControllerFactory =
new SpecialEffectsControllerFactory() {
@NonNull
@Override
public SpecialEffectsController createController(@NonNull ViewGroup container) {
return new DefaultSpecialEffectsController(container);
}
};
private ActivityResultLauncher<Intent> mStartActivityForResult;
private ActivityResultLauncher<IntentSenderRequest> mStartIntentSenderForResult;
private ActivityResultLauncher<String[]> mRequestPermissions;
ArrayDeque<LaunchedFragmentInfo> mLaunchedFragments = new ArrayDeque<>();
private static final String EXTRA_CREATED_FILLIN_INTENT = "androidx.fragment"
+ ".extra.ACTIVITY_OPTIONS_BUNDLE";
private boolean mNeedMenuInvalidate;
private boolean mStateSaved;
private boolean mStopped;
private boolean mDestroyed;
private boolean mHavePendingDeferredStart;
// Temporary vars for removing redundant operations in BackStackRecords:
private ArrayList<BackStackRecord> mTmpRecords;
private ArrayList<Boolean> mTmpIsPop;
private ArrayList<Fragment> mTmpAddedFragments;
private FragmentManagerViewModel mNonConfig;
private FragmentStrictMode.Policy mStrictModePolicy;
private Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions(true);
}
};
private void throwException(RuntimeException ex) {
Log.e(TAG, ex.getMessage());
Log.e(TAG, "Activity state:");
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
if (mHost != null) {
try {
mHost.onDump(" ", null, pw, new String[] { });
} catch (Exception e) {
Log.e(TAG, "Failed dumping state", e);
}
} else {
try {
dump(" ", null, pw, new String[] { });
} catch (Exception e) {
Log.e(TAG, "Failed dumping state", e);
}
}
throw ex;
}
/**
* @deprecated Use {@link #beginTransaction()}.
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@Deprecated
@NonNull
public FragmentTransaction openTransaction() {
return beginTransaction();
}
/**
* Start a series of edit operations on the Fragments associated with
* this FragmentManager.
*
* <p>Note: A fragment transaction can only be created/committed prior
* to an activity saving its state. If you try to commit a transaction
* after {@link FragmentActivity#onSaveInstanceState FragmentActivity.onSaveInstanceState()}
* (and prior to a following {@link FragmentActivity#onStart FragmentActivity.onStart}
* or {@link FragmentActivity#onResume FragmentActivity.onResume()}, you will get an error.
* This is because the framework takes care of saving your current fragments
* in the state, and if changes are made after the state is saved then they
* will be lost.</p>
*/
@NonNull
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
/**
* After a {@link FragmentTransaction} is committed with
* {@link FragmentTransaction#commit FragmentTransaction.commit()}, it
* is scheduled to be executed asynchronously on the process's main thread.
* If you want to immediately executing any such pending operations, you
* can call this function (only from the main thread) to do so. Note that
* all callbacks and other related behavior will be done from within this
* call, so be careful about where this is called from.
*
* <p>If you are committing a single transaction that does not modify the
* fragment back stack, strongly consider using
* {@link FragmentTransaction#commitNow()} instead. This can help avoid
* unwanted side effects when other code in your app has pending committed
* transactions that expect different timing.</p>
* <p>
* This also forces the start of any postponed Transactions where
* {@link Fragment#postponeEnterTransition()} has been called.
*
* @return Returns true if there were any pending transactions to be
* executed.
*/
@MainThread
public boolean executePendingTransactions() {
boolean updates = execPendingActions(true);
forcePostponedTransactions();
return updates;
}
private void updateOnBackPressedCallbackEnabled() {
// Always enable the callback if we have pending actions
// as we don't know if they'll change the back stack entry count.
// See handleOnBackPressed() for more explanation
synchronized (mPendingActions) {
if (!mPendingActions.isEmpty()) {
mOnBackPressedCallback.setEnabled(true);
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "FragmentManager " + FragmentManager.this + " enabling "
+ "OnBackPressedCallback, caused by non-empty pending actions");
}
return;
}
}
// This FragmentManager needs to have a back stack for this to be enabled
// And the parent fragment, if it exists, needs to be the primary navigation
// fragment.
boolean isEnabled = getBackStackEntryCount() > 0
&& isPrimaryNavigation(mParent);
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG,
"OnBackPressedCallback for FragmentManager " + this + " enabled state is "
+ isEnabled
);
}
mOnBackPressedCallback.setEnabled(isEnabled);
}
/**
* Recursively check up the FragmentManager hierarchy of primary
* navigation Fragments to ensure that all of the parent Fragments are the
* primary navigation Fragment for their associated FragmentManager
*/
boolean isPrimaryNavigation(@Nullable Fragment parent) {
// If the parent is null, then we're at the root host
// and we're always the primary navigation
if (parent == null) {
return true;
}
FragmentManager parentFragmentManager = parent.mFragmentManager;
Fragment primaryNavigationFragment = parentFragmentManager
.getPrimaryNavigationFragment();
// The parent Fragment needs to be the primary navigation Fragment
// and, if it has a parent itself, that parent also needs to be
// the primary navigation fragment, recursively up the stack
return parent.equals(primaryNavigationFragment)
&& isPrimaryNavigation(parentFragmentManager.mParent);
}
/**
* Recursively check up the FragmentManager hierarchy of Fragments to see
* if the menus are all visible.
*/
boolean isParentMenuVisible(@Nullable Fragment parent) {
if (parent == null) {
return true;
}
return parent.isMenuVisible();
}
/**
* Recursively check up the FragmentManager hierarchy of Fragments to see
* if the fragment is hidden.
*/
boolean isParentHidden(@Nullable Fragment parent) {
if (parent == null) {
return false;
}
return parent.isHidden();
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void handleOnBackPressed() {
// First, execute any pending actions to make sure we're in an
// up to date view of the world just in case anyone is queuing
// up transactions that change the back stack then immediately
// calling onBackPressed()
execPendingActions(true);
if (USE_PREDICTIVE_BACK && mTransitioningOp != null) {
if (!mBackStackChangeListeners.isEmpty()) {
// Build a list of fragments based on the records
Set<Fragment> fragments = new LinkedHashSet<>(
fragmentsFromRecord(mTransitioningOp));
// Dispatch to all of the fragments in the list
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
// We give all fragment the back stack changed started signal first
for (Fragment fragment : fragments) {
listener.onBackStackChangeCommitted(fragment, true);
}
}
}
for (FragmentTransaction.Op op : mTransitioningOp.mOps) {
if (op.mFragment != null) {
op.mFragment.mTransitioning = false;
}
}
Set<SpecialEffectsController> changedControllers = collectChangedControllers(
new ArrayList<>(Collections.singletonList(mTransitioningOp)), 0, 1
);
for (SpecialEffectsController controller : changedControllers) {
controller.completeBack();
}
mTransitioningOp = null;
updateOnBackPressedCallbackEnabled();
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "Op is being set to null");
Log.d(TAG, "OnBackPressedCallback enabled=" + mOnBackPressedCallback.isEnabled()
+ " for FragmentManager " + this);
}
} else {
if (mOnBackPressedCallback.isEnabled()) {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "Calling popBackStackImmediate via onBackPressed callback");
}
// We still have a back stack, so we can pop
popBackStackImmediate();
} else {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "Calling onBackPressed via onBackPressed callback");
}
// Sigh. Due to FragmentManager's asynchronicity, we can
// get into cases where we *think* we can handle the back
// button but because of frame perfect dispatch, we fell
// on our face. Since our callback is disabled, we can
// re-trigger the onBackPressed() to dispatch to the next
// enabled callback
mOnBackPressedDispatcher.onBackPressed();
}
}
}
/**
* Restores the back stack previously saved via {@link #saveBackStack(String)}. This
* will result in all of the transactions that made up that back stack to be re-executed,
* thus re-adding any fragments that were added through those transactions. All state of
* those fragments will be restored as part of this process. If no state was previously
* saved with the given name, this operation does nothing.
* <p>
* This function is asynchronous -- it enqueues the
* request to restore, but the action will not be performed until the application
* returns to its event loop.
*
* @param name The name of the back stack previously saved by {@link #saveBackStack(String)}.
*/
public void restoreBackStack(@NonNull String name) {
enqueueAction(new RestoreBackStackState(name), false);
}
/**
* Save the back stack. While this functions similarly to
* {@link #popBackStack(String, int)}, it <strong>does not</strong> throw away the
* state of any fragments that were added through those transactions. Instead, the
* back stack that is saved by this method can later be restored with its state
* in tact.
* <p>
* This function is asynchronous -- it enqueues the
* request to pop, but the action will not be performed until the application
* returns to its event loop.
*
* @param name The name set by {@link FragmentTransaction#addToBackStack(String)}.
*/
public void saveBackStack(@NonNull String name) {
enqueueAction(new SaveBackStackState(name), false);
}
/**
* Clears the back stack previously saved via {@link #saveBackStack(String)}. This
* will result in all of the transactions that made up that back stack to be thrown away,
* thus destroying any fragments that were added through those transactions. All state of
* those fragments will be cleared as part of this process. If no state was previously
* saved with the given name, this operation does nothing.
* <p>
* This function is asynchronous -- it enqueues the
* request to clear, but the action will not be performed until the application
* returns to its event loop.
*
* @param name The name of the back stack previously saved by {@link #saveBackStack(String)}.
*/
public void clearBackStack(@NonNull String name) {
enqueueAction(new ClearBackStackState(name), false);
}
/**
* Pop the top state off the back stack. This function is asynchronous -- it enqueues the
* request to pop, but the action will not be performed until the application
* returns to its event loop.
*/
public void popBackStack() {
enqueueAction(new PopBackStackState(null, -1, 0), false);
}
/**
* Like {@link #popBackStack()}, but performs the operation immediately
* inside of the call. This is like calling {@link #executePendingTransactions()}
* afterwards without forcing the start of postponed Transactions.
* @return Returns true if there was something popped, else false.
*/
@MainThread
public boolean popBackStackImmediate() {
return popBackStackImmediate(null, -1, 0);
}
/**
* Pop the last fragment transition from the manager's fragment
* back stack.
* This function is asynchronous -- it enqueues the
* request to pop, but the action will not be performed until the application
* returns to its event loop.
*
* @param name If non-null, this is the name of a previous back state
* to look for; if found, all states up to that state will be popped. The
* {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
* the named state itself is popped. If null, only the top state is popped.
* @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.
*/
public void popBackStack(@Nullable final String name, final int flags) {
enqueueAction(new PopBackStackState(name, -1, flags), false);
}
/**
* Like {@link #popBackStack(String, int)}, but performs the operation immediately
* inside of the call. This is like calling {@link #executePendingTransactions()}
* afterwards without forcing the start of postponed Transactions.
* @return Returns true if there was something popped, else false.
*/
@MainThread
public boolean popBackStackImmediate(@Nullable String name, int flags) {
return popBackStackImmediate(name, -1, flags);
}
/**
* Pop all back stack states up to the one with the given identifier.
* This function is asynchronous -- it enqueues the
* request to pop, but the action will not be performed until the application
* returns to its event loop.
*
* @param id Identifier of the stated to be popped. If no identifier exists,
* false is returned.
* The identifier is the number returned by
* {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The
* {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
* the named state itself is popped.
* @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.
*/
public void popBackStack(final int id, final int flags) {
popBackStack(id, flags, false);
}
void popBackStack(final int id, final int flags, boolean allowStateLoss) {
if (id < 0) {
throw new IllegalArgumentException("Bad id: " + id);
}
enqueueAction(new PopBackStackState(null, id, flags), allowStateLoss);
}
void prepareBackStackTransition() {
enqueueAction(new PrepareBackStackTransitionState(), false);
}
void cancelBackStackTransition() {
if (mTransitioningOp != null) {
mTransitioningOp.mCommitted = false;
mTransitioningOp.runOnCommitInternal(true, () -> {
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
listener.onBackStackChangeCancelled();
}
});
mTransitioningOp.commit();
executePendingTransactions();
}
}
/**
* Like {@link #popBackStack(int, int)}, but performs the operation immediately
* inside of the call. This is like calling {@link #executePendingTransactions()}
* afterwards without forcing the start of postponed Transactions.
* @return Returns true if there was something popped, else false.
*/
public boolean popBackStackImmediate(int id, int flags) {
if (id < 0) {
throw new IllegalArgumentException("Bad id: " + id);
}
return popBackStackImmediate(null, id, flags);
}
/**
* Used by all public popBackStackImmediate methods, this executes pending transactions and
* returns true if the pop action did anything, regardless of what other pending
* transactions did.
*
* @return true if the pop operation did anything or false otherwise.
*/
private boolean popBackStackImmediate(@Nullable String name, int id, int flags) {
execPendingActions(false);
ensureExecReady(true);
if (mPrimaryNav != null // We have a primary nav fragment
&& id < 0 // No valid id (since they're local)
&& name == null) { // no name to pop to (since they're local)
final FragmentManager childManager = mPrimaryNav.getChildFragmentManager();
if (childManager.popBackStackImmediate()) {
// We did something, just not to this specific FragmentManager. Return true.
return true;
}
}
boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags);
if (executePop) {
mExecutingActions = true;
try {
removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
} finally {
cleanupExec();
}
}
updateOnBackPressedCallbackEnabled();
doPendingDeferredStart();
mFragmentStore.burpActive();
return executePop;
}
/**
* Return the number of entries currently in the back stack.
*/
public int getBackStackEntryCount() {
return mBackStack.size() + (mTransitioningOp != null ? 1 : 0);
}
/**
* Return the BackStackEntry at index <var>index</var> in the back stack;
* entries start index 0 being the bottom of the stack.
*/
@NonNull
public BackStackEntry getBackStackEntryAt(int index) {
if (index == mBackStack.size()) {
if (mTransitioningOp == null) {
throw new IndexOutOfBoundsException();
}
return mTransitioningOp;
}
return mBackStack.get(index);
}
/**
* Add a new listener for changes to the fragment back stack.
*/
public void addOnBackStackChangedListener(@NonNull OnBackStackChangedListener listener) {
mBackStackChangeListeners.add(listener);
}
/**
* Remove a listener that was previously added with
* {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}.
*/
public void removeOnBackStackChangedListener(@NonNull OnBackStackChangedListener listener) {
mBackStackChangeListeners.remove(listener);
}
@Override
public final void setFragmentResult(@NonNull String requestKey, @NonNull Bundle result) {
// Check if there is a listener waiting for a result with this key
LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
// if there is and it is started, fire the callback
if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
resultListener.onFragmentResult(requestKey, result);
} else {
// else, save the result for later
mResults.put(requestKey, result);
}
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG, "Setting fragment result with key " + requestKey + " and "
+ "result " + result);
}
}
@Override
public final void clearFragmentResult(@NonNull String requestKey) {
mResults.remove(requestKey);
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG, "Clearing fragment result with key " + requestKey);
}
}
@Override
public final void setFragmentResultListener(@NonNull final String requestKey,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final FragmentResultListener listener) {
final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
return;
}
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_START) {
// once we are started, check for any stored results
Bundle storedResult = mResults.get(requestKey);
if (storedResult != null) {
// if there is a result, fire the callback
listener.onFragmentResult(requestKey, storedResult);
// and clear the result
clearFragmentResult(requestKey);
}
}
if (event == Lifecycle.Event.ON_DESTROY) {
lifecycle.removeObserver(this);
mResultListeners.remove(requestKey);
}
}
};
LifecycleAwareResultListener storedListener = mResultListeners.put(requestKey,
new LifecycleAwareResultListener(lifecycle, listener, observer));
if (storedListener != null) {
storedListener.removeObserver();
}
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG, "Setting FragmentResultListener with key " + requestKey
+ " lifecycleOwner " + lifecycle + " and listener " + listener);
}
// Only add the observer after we've added the listener to the map
// to ensure that re-entrant removals actually have a registered listener to remove
lifecycle.addObserver(observer);
}
@Override
public final void clearFragmentResultListener(@NonNull String requestKey) {
LifecycleAwareResultListener listener = mResultListeners.remove(requestKey);
if (listener != null) {
listener.removeObserver();
}
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(FragmentManager.TAG, "Clearing FragmentResultListener for key " + requestKey);
}
}
/**
* Put a reference to a fragment in a Bundle. This Bundle can be
* persisted as saved state, and when later restoring
* {@link #getFragment(Bundle, String)} will return the current
* instance of the same fragment.
*
* @param bundle The bundle in which to put the fragment reference.
* @param key The name of the entry in the bundle.
* @param fragment The Fragment whose reference is to be stored.
*/
public void putFragment(@NonNull Bundle bundle, @NonNull String key,
@NonNull Fragment fragment) {
if (fragment.mFragmentManager != this) {
throwException(new IllegalStateException("Fragment " + fragment
+ " is not currently in the FragmentManager"));
}
bundle.putString(key, fragment.mWho);
}
/**
* Retrieve the current Fragment instance for a reference previously
* placed with {@link #putFragment(Bundle, String, Fragment)}.
*
* @param bundle The bundle from which to retrieve the fragment reference.
* @param key The name of the entry in the bundle.
* @return Returns the current Fragment instance that is associated with
* the given reference.
*/
@Nullable
public Fragment getFragment(@NonNull Bundle bundle, @NonNull String key) {
String who = bundle.getString(key);
if (who == null) {
return null;
}
Fragment f = findActiveFragment(who);
if (f == null) {
throwException(new IllegalStateException("Fragment no longer exists for key "
+ key + ": unique id " + who));
}
return f;
}
/**
* Find a {@link Fragment} associated with the given {@link View}.
*
* This method will locate the {@link Fragment} associated with this view. This is automatically
* populated for the View returned by {@link Fragment#onCreateView} and its children.
*
* @param view the view to search from
* @return the locally scoped {@link Fragment} to the given view
* @throws IllegalStateException if the given view does not correspond with a
* {@link Fragment}.
*/
@NonNull
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) // We should throw a ClassCast
// exception if the type is wrong
public static <F extends Fragment> F findFragment(@NonNull View view) {
Fragment fragment = findViewFragment(view);
if (fragment == null) {
throw new IllegalStateException("View " + view + " does not have a Fragment set");
}
return (F) fragment;
}
/**
* Recurse up the view hierarchy, looking for the Fragment
* @param view the view to search from
* @return the locally scoped {@link Fragment} to the given view, if found
*/
@Nullable
static Fragment findViewFragment(@NonNull View view) {
while (view != null) {
Fragment fragment = getViewFragment(view);
if (fragment != null) {
return fragment;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}
/**
* Check if this view has an associated Fragment
* @param view the view to search from
* @return the locally scoped {@link Fragment} to the given view, if found
*/
@Nullable
static Fragment getViewFragment(@NonNull View view) {
Object tag = view.getTag(R.id.fragment_container_view_tag);
if (tag instanceof Fragment) {
return (Fragment) tag;
}
return null;
}
/**
* Callback for when the {@link FragmentContainerView} becomes available in the view hierarchy
* and the fragment manager can add the fragment view to its hierarchy.
*
* @param container the container that the active fragment should add their views to
*/
public final void onContainerAvailable(@NonNull FragmentContainerView container) {
for (FragmentStateManager fragmentStateManager:
mFragmentStore.getActiveFragmentStateManagers()) {
Fragment fragment = fragmentStateManager.getFragment();
if (fragment.mContainerId == container.getId() && fragment.mView != null
&& fragment.mView.getParent() == null
) {
fragment.mContainer = container;
fragmentStateManager.addViewToContainer();
}
}
}
/**
* Recurse up the view hierarchy, looking for a FragmentManager
*
* @param view the view to search from
* @return The containing {@link FragmentManager} of the given view.
* @throws IllegalStateException if there no Fragment associated with the view and the
* view's context is not a {@link FragmentActivity}.
*/
@NonNull
public static FragmentManager findFragmentManager(@NonNull View view) {
// Search the view ancestors for a Fragment
Fragment fragment = findViewFragment(view);
FragmentManager fm;
// If there is a Fragment in the hierarchy, get its childFragmentManager, otherwise
// use the fragmentManager of the Activity.
if (fragment != null) {
if (!fragment.isAdded()) {
throw new IllegalStateException("The Fragment " + fragment + " that owns View "
+ view + " has already been destroyed. Nested fragments should always "
+ "use the child FragmentManager.");
}
fm = fragment.getChildFragmentManager();
} else {
Context context = view.getContext();
FragmentActivity fragmentActivity = null;
while (context instanceof ContextWrapper) {
if (context instanceof FragmentActivity) {
fragmentActivity = (FragmentActivity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
if (fragmentActivity != null) {
fm = fragmentActivity.getSupportFragmentManager();
} else {
throw new IllegalStateException("View " + view + " is not within a subclass of "
+ "FragmentActivity.");
}
}
return fm;
}
/**
* Get a list of all fragments that are currently added to the FragmentManager.
* This may include those that are hidden as well as those that are shown.
* This will not include any fragments only in the back stack, or fragments that
* are detached or removed.
* <p>
* The order of the fragments in the list is the order in which they were
* added or attached.
*
* @return A list of all fragments that are added to the FragmentManager.
*/
@NonNull
@SuppressWarnings("unchecked")
public List<Fragment> getFragments() {
return mFragmentStore.getFragments();
}
@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
return mNonConfig.getViewModelStore(f);
}
@NonNull
private FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {
return mNonConfig.getChildNonConfig(f);
}
void addRetainedFragment(@NonNull Fragment f) {
mNonConfig.addRetainedFragment(f);
}
void removeRetainedFragment(@NonNull Fragment f) {
mNonConfig.removeRetainedFragment(f);
}
/**
* This is used by FragmentController to get the Active fragments.
*
* @return A list of active fragments in the fragment manager, including those that are in the
* back stack.
*/
@NonNull
List<Fragment> getActiveFragments() {
return mFragmentStore.getActiveFragments();
}
/**
* Used by FragmentController to get the number of Active Fragments.
*
* @return The number of active fragments.
*/
int getActiveFragmentCount() {
return mFragmentStore.getActiveFragmentCount();
}
/**
* Save the current instance state of the given Fragment. This can be
* used later when creating a new instance of the Fragment and adding
* it to the fragment manager, to have it create itself to match the
* current state returned here. Note that there are limits on how
* this can be used:
*
* <ul>
* <li>The Fragment must currently be attached to the FragmentManager.
* <li>A new Fragment created using this saved state must be the same class
* type as the Fragment it was created from.
* <li>The saved state can not contain dependencies on other fragments --
* that is it can't use {@link #putFragment(Bundle, String, Fragment)} to
* store a fragment reference because that reference may not be valid when
* this saved state is later used. Likewise the Fragment's target and
* result code are not included in this state.
* </ul>
*
* @param fragment The Fragment whose state is to be saved.
* @return The generated state. This will be null if there was no
* interesting state created by the fragment.
*/
@Nullable
public Fragment.SavedState saveFragmentInstanceState(@NonNull Fragment fragment) {
FragmentStateManager fragmentStateManager = mFragmentStore.getFragmentStateManager(
fragment.mWho);
if (fragmentStateManager == null || !fragmentStateManager.getFragment().equals(fragment)) {
throwException(new IllegalStateException("Fragment " + fragment
+ " is not currently in the FragmentManager"));
}
return fragmentStateManager.saveInstanceState();
}
private void clearBackStackStateViewModels() {
boolean shouldClear;
if (mHost instanceof ViewModelStoreOwner) {
shouldClear = mFragmentStore.getNonConfig().isCleared();
} else if (mHost.getContext() instanceof Activity) {
Activity activity = (Activity) mHost.getContext();
shouldClear = !activity.isChangingConfigurations();
} else {
shouldClear = true;
}
if (shouldClear) {
for (BackStackState backStackState : mBackStackStates.values()) {
for (String who : backStackState.mFragments) {
mFragmentStore.getNonConfig().clearNonConfigState(who, false);
}
}
}
}
/**
* Returns true if the final {@link android.app.Activity#onDestroy() Activity.onDestroy()}
* call has been made on the FragmentManager's Activity, so this instance is now dead.
*/
public boolean isDestroyed() {
return mDestroyed;
}
@NonNull
@Override
public String toString() {
StringBuilder sb = new StringBuilder(128);
sb.append("FragmentManager{");
sb.append(Integer.toHexString(System.identityHashCode(this)));
sb.append(" in ");
if (mParent != null) {
Class<?> cls = mParent.getClass();
sb.append(cls.getSimpleName());
sb.append("{");
sb.append(Integer.toHexString(System.identityHashCode(mParent)));
sb.append("}");
} else if (mHost != null) {
Class<?> cls = mHost.getClass();
sb.append(cls.getSimpleName());
sb.append("{");
sb.append(Integer.toHexString(System.identityHashCode(mHost)));
sb.append("}");
} else {
sb.append("null");
}
sb.append("}}");
return sb.toString();
}
/**
* Print the FragmentManager's state into the given stream.
*
* @param prefix Text to print at the front of each line.
* @param fd The raw file descriptor that the dump is being sent to.
* @param writer A PrintWriter to which the dump is to be set.
* @param args Additional arguments to the dump request.
*/
public void dump(@NonNull String prefix, @Nullable FileDescriptor fd,
@NonNull PrintWriter writer, @Nullable String[] args) {
String innerPrefix = prefix + " ";
mFragmentStore.dump(prefix, fd, writer, args);
int count;
if (mCreatedMenus != null) {
count = mCreatedMenus.size();
if (count > 0) {
writer.print(prefix); writer.println("Fragments Created Menus:");
for (int i = 0; i < count; i++) {
Fragment f = mCreatedMenus.get(i);
writer.print(prefix);
writer.print(" #");
writer.print(i);
writer.print(": ");
writer.println(f.toString());
}
}
}
count = mBackStack.size();
if (count > 0) {
writer.print(prefix); writer.println("Back Stack:");
for (int i = 0; i < count; i++) {
BackStackRecord bs = mBackStack.get(i);
writer.print(prefix);
writer.print(" #");
writer.print(i);
writer.print(": ");
writer.println(bs.toString());
bs.dump(innerPrefix, writer);
}
}
writer.print(prefix);
writer.println("Back Stack Index: " + mBackStackIndex.get());
synchronized (mPendingActions) {
count = mPendingActions.size();
if (count > 0) {
writer.print(prefix); writer.println("Pending Actions:");
for (int i = 0; i < count; i++) {
OpGenerator r = mPendingActions.get(i);
writer.print(prefix);
writer.print(" #");
writer.print(i);
writer.print(": ");
writer.println(r);
}
}
}
writer.print(prefix);
writer.println("FragmentManager misc state:");
writer.print(prefix);
writer.print(" mHost=");
writer.println(mHost);
writer.print(prefix);
writer.print(" mContainer=");
writer.println(mContainer);
if (mParent != null) {
writer.print(prefix);
writer.print(" mParent=");
writer.println(mParent);
}
writer.print(prefix);
writer.print(" mCurState=");
writer.print(mCurState);
writer.print(" mStateSaved=");
writer.print(mStateSaved);
writer.print(" mStopped=");
writer.print(mStopped);
writer.print(" mDestroyed=");
writer.println(mDestroyed);
if (mNeedMenuInvalidate) {
writer.print(prefix);
writer.print(" mNeedMenuInvalidate=");
writer.println(mNeedMenuInvalidate);
}
}
void performPendingDeferredStart(@NonNull FragmentStateManager fragmentStateManager) {
Fragment f = fragmentStateManager.getFragment();
if (f.mDeferStart) {
if (mExecutingActions) {
// Wait until we're done executing our pending transactions
mHavePendingDeferredStart = true;
return;
}
f.mDeferStart = false;
fragmentStateManager.moveToExpectedState();
}
}
boolean isStateAtLeast(int state) {
return mCurState >= state;
}
/**
* Allows for changing the draw order on a container, if the container is a
* FragmentContainerView.
*/
void setExitAnimationOrder(@NonNull Fragment f, boolean isPop) {
ViewGroup container = getFragmentContainer(f);
if (container != null) {
if (container instanceof FragmentContainerView) {
((FragmentContainerView) container).setDrawDisappearingViewsLast(!isPop);
}
}
}
/**
* Changes the state of the fragment manager to {@code newState}. If the fragment manager
* changes state or {@code always} is {@code true}, any fragments within it have their
* states updated as well.
*
* @param newState The new state for the fragment manager
* @param always If {@code true}, all fragments update their state, even
* if {@code newState} matches the current fragment manager's state.
*/
void moveToState(int newState, boolean always) {
if (mHost == null && newState != Fragment.INITIALIZING) {
throw new IllegalStateException("No activity");
}
if (!always && newState == mCurState) {
return;
}
mCurState = newState;
mFragmentStore.moveToExpectedState();
startPendingDeferredFragments();
if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) {
mHost.onSupportInvalidateOptionsMenu();
mNeedMenuInvalidate = false;
}
}
private void startPendingDeferredFragments() {
for (FragmentStateManager fragmentStateManager :
mFragmentStore.getActiveFragmentStateManagers()) {
performPendingDeferredStart(fragmentStateManager);
}
}
/**
* For a given Fragment, get any existing FragmentStateManager found in the
* {@link FragmentStore} or create a brand new FragmentStateManager if one does
* not exist.
*
* @param f The Fragment to create a FragmentStateManager for
* @return A valid FragmentStateManager
*/
@NonNull
FragmentStateManager createOrGetFragmentStateManager(@NonNull Fragment f) {
FragmentStateManager existing = mFragmentStore.getFragmentStateManager(f.mWho);
if (existing != null) {
return existing;
}
FragmentStateManager fragmentStateManager = new FragmentStateManager(
mLifecycleCallbacksDispatcher, mFragmentStore, f);
// Restore state any state set via setInitialSavedState()
fragmentStateManager.restoreState(mHost.getContext().getClassLoader());
// Catch the FragmentStateManager up to our current state
fragmentStateManager.setFragmentManagerState(mCurState);
return fragmentStateManager;
}
FragmentStateManager addFragment(@NonNull Fragment fragment) {
if (fragment.mPreviousWho != null) {
FragmentStrictMode.onFragmentReuse(fragment, fragment.mPreviousWho);
}
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "add: " + fragment);
FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
fragment.mFragmentManager = this;
mFragmentStore.makeActive(fragmentStateManager);
if (!fragment.mDetached) {
mFragmentStore.addFragment(fragment);
fragment.mRemoving = false;
if (fragment.mView == null) {
fragment.mHiddenChanged = false;
}
if (isMenuAvailable(fragment)) {
mNeedMenuInvalidate = true;
}
}
return fragmentStateManager;
}
void removeFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);
}
final boolean inactive = !fragment.isInBackStack();
if (!fragment.mDetached || inactive) {
mFragmentStore.removeFragment(fragment);
if (isMenuAvailable(fragment)) {
mNeedMenuInvalidate = true;
}
fragment.mRemoving = true;
setVisibleRemovingFragment(fragment);
}
}
/**
* Marks a fragment as hidden to be later animated.
*
* @param fragment The fragment to be shown.
*/
void hideFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "hide: " + fragment);
if (!fragment.mHidden) {
fragment.mHidden = true;
// Toggle hidden changed so that if a fragment goes through show/hide/show
// it doesn't go through the animation.
fragment.mHiddenChanged = !fragment.mHiddenChanged;
setVisibleRemovingFragment(fragment);
}
}
/**
* Marks a fragment as shown to be later animated.
*
* @param fragment The fragment to be shown.
*/
void showFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "show: " + fragment);
if (fragment.mHidden) {
fragment.mHidden = false;
// Toggle hidden changed so that if a fragment goes through show/hide/show
// it doesn't go through the animation.
fragment.mHiddenChanged = !fragment.mHiddenChanged;
}
}
void detachFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "detach: " + fragment);
if (!fragment.mDetached) {
fragment.mDetached = true;
if (fragment.mAdded) {
// We are not already in back stack, so need to remove the fragment.
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "remove from detach: " + fragment);
mFragmentStore.removeFragment(fragment);
if (isMenuAvailable(fragment)) {
mNeedMenuInvalidate = true;
}
setVisibleRemovingFragment(fragment);
}
}
}
void attachFragment(@NonNull Fragment fragment) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "attach: " + fragment);
if (fragment.mDetached) {
fragment.mDetached = false;
if (!fragment.mAdded) {
mFragmentStore.addFragment(fragment);
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "add from attach: " + fragment);
if (isMenuAvailable(fragment)) {
mNeedMenuInvalidate = true;
}
}
}
}
/**
* Finds a fragment that was identified by the given id either when inflated
* from XML or as the container ID when added in a transaction. This first
* searches through fragments that are currently added to the manager's
* activity; if no such fragment is found, then all fragments currently
* on the back stack associated with this ID are searched.
* @return The fragment if found or null otherwise.
*/
@Nullable
public Fragment findFragmentById(@IdRes int id) {
return mFragmentStore.findFragmentById(id);
}
/**
* Finds a fragment that was identified by the given tag either when inflated
* from XML or as supplied when added in a transaction. This first
* searches through fragments that are currently added to the manager's
* activity; if no such fragment is found, then all fragments currently
* on the back stack are searched.
* <p>
* If provided a {@code null} tag, this method returns null.
*
* @param tag the tag used to search for the fragment
* @return The fragment if found or null otherwise.
*/
@Nullable
public Fragment findFragmentByTag(@Nullable String tag) {
return mFragmentStore.findFragmentByTag(tag);
}
Fragment findFragmentByWho(@NonNull String who) {
return mFragmentStore.findFragmentByWho(who);
}
@Nullable
Fragment findActiveFragment(@NonNull String who) {
return mFragmentStore.findActiveFragment(who);
}
private void checkStateLoss() {
if (isStateSaved()) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
}
/**
* Returns {@code true} if the FragmentManager's state has already been saved
* by its host. Any operations that would change saved state should not be performed
* if this method returns true. For example, any popBackStack() method, such as
* {@link #popBackStackImmediate()} or any FragmentTransaction using
* {@link FragmentTransaction#commit()} instead of
* {@link FragmentTransaction#commitAllowingStateLoss()} will change
* the state and will result in an error.
*
* @return true if this FragmentManager's state has already been saved by its host
*/
public boolean isStateSaved() {
// See saveAllState() for the explanation of this. We do this for
// all platform versions, to keep our behavior more consistent between
// them.
return mStateSaved || mStopped;
}
/**
* Adds an action to the queue of pending actions.
*
* @param action the action to add
* @param allowStateLoss whether to allow loss of state information
* @throws IllegalStateException if the activity has been destroyed
*/
void enqueueAction(@NonNull OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
if (mHost == null) {
if (mDestroyed) {
throw new IllegalStateException("FragmentManager has been destroyed");
} else {
throw new IllegalStateException("FragmentManager has not been attached to a "
+ "host.");
}
}
checkStateLoss();
}
synchronized (mPendingActions) {
if (mHost == null) {
if (allowStateLoss) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
throw new IllegalStateException("Activity has been destroyed");
}
mPendingActions.add(action);
scheduleCommit();
}
}
/**
* Schedules the execution when one hasn't been scheduled already. This should happen
* the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when
* a postponed transaction has been started with
* {@link Fragment#startPostponedEnterTransition()}
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void scheduleCommit() {
synchronized (mPendingActions) {
boolean pendingReady = mPendingActions.size() == 1;
if (pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
updateOnBackPressedCallbackEnabled();
}
}
}
int allocBackStackIndex() {
return mBackStackIndex.getAndIncrement();
}
/**
* Broken out from exec*, this prepares for gathering and executing operations.
*
* @param allowStateLoss true if state loss should be ignored or false if it should be
* checked.
*/
private void ensureExecReady(boolean allowStateLoss) {
if (mExecutingActions) {
throw new IllegalStateException("FragmentManager is already executing transactions");
}
if (mHost == null) {
if (mDestroyed) {
throw new IllegalStateException("FragmentManager has been destroyed");
} else {
throw new IllegalStateException("FragmentManager has not been attached to a host.");
}
}
if (Looper.myLooper() != mHost.getHandler().getLooper()) {
throw new IllegalStateException("Must be called from main thread of fragment host");
}
if (!allowStateLoss) {
checkStateLoss();
}
if (mTmpRecords == null) {
mTmpRecords = new ArrayList<>();
mTmpIsPop = new ArrayList<>();
}
}
void execSingleAction(@NonNull OpGenerator action, boolean allowStateLoss) {
if (allowStateLoss && (mHost == null || mDestroyed)) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
ensureExecReady(allowStateLoss);
if (action.generateOps(mTmpRecords, mTmpIsPop)) {
mExecutingActions = true;
try {
removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
} finally {
cleanupExec();
}
}
updateOnBackPressedCallbackEnabled();
doPendingDeferredStart();
mFragmentStore.burpActive();
}
/**
* Broken out of exec*, this cleans up the mExecutingActions and the temporary structures
* used in executing operations.
*/
private void cleanupExec() {
mExecutingActions = false;
mTmpIsPop.clear();
mTmpRecords.clear();
}
/**
* Only call from main thread!
*/
boolean execPendingActions(boolean allowStateLoss) {
ensureExecReady(allowStateLoss);
boolean didSomething = false;
while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
mExecutingActions = true;
try {
removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
} finally {
cleanupExec();
}
didSomething = true;
}
updateOnBackPressedCallbackEnabled();
doPendingDeferredStart();
mFragmentStore.burpActive();
return didSomething;
}
/**
* Remove redundant BackStackRecord operations and executes them. This method merges operations
* of proximate records that allow reordering. See
* {@link FragmentTransaction#setReorderingAllowed(boolean)}.
* <p>
* For example, a transaction that adds to the back stack and then another that pops that
* back stack record will be optimized to remove the unnecessary operation.
* <p>
* Likewise, two transactions committed that are executed at the same time will be optimized
* to remove the redundant operations as well as two pop operations executed together.
*
* @param records The records pending execution
* @param isRecordPop The direction that these records are being run.
*/
private void removeRedundantOperationsAndExecute(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
if (records.isEmpty()) {
return;
}
if (records.size() != isRecordPop.size()) {
throw new IllegalStateException("Internal error with the back stack records");
}
final int numRecords = records.size();
int startIndex = 0;
for (int recordNum = 0; recordNum < numRecords; recordNum++) {
final boolean canReorder = records.get(recordNum).mReorderingAllowed;
if (!canReorder) {
// execute all previous transactions
if (startIndex != recordNum) {
executeOpsTogether(records, isRecordPop, startIndex, recordNum);
}
// execute all pop operations that don't allow reordering together or
// one add operation
int reorderingEnd = recordNum + 1;
if (isRecordPop.get(recordNum)) {
while (reorderingEnd < numRecords
&& isRecordPop.get(reorderingEnd)
&& !records.get(reorderingEnd).mReorderingAllowed) {
reorderingEnd++;
}
}
executeOpsTogether(records, isRecordPop, recordNum, reorderingEnd);
startIndex = reorderingEnd;
recordNum = reorderingEnd - 1;
}
}
if (startIndex != numRecords) {
executeOpsTogether(records, isRecordPop, startIndex, numRecords);
}
}
/**
* Executes a subset of a list of BackStackRecords, all of which either allow reordering or
* do not allow ordering.
* @param records A list of BackStackRecords that are to be executed
* @param isRecordPop The direction that these records are being run.
* @param startIndex The index of the first record in <code>records</code> to be executed
* @param endIndex One more than the final record index in <code>records</code> to executed.
*/
private void executeOpsTogether(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
final boolean allowReordering = records.get(startIndex).mReorderingAllowed;
boolean addToBackStack = false;
if (mTmpAddedFragments == null) {
mTmpAddedFragments = new ArrayList<>();
} else {
mTmpAddedFragments.clear();
}
mTmpAddedFragments.addAll(mFragmentStore.getFragments());
Fragment oldPrimaryNav = getPrimaryNavigationFragment();
for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
final BackStackRecord record = records.get(recordNum);
final boolean isPop = isRecordPop.get(recordNum);
if (!isPop) {
oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav);
} else {
oldPrimaryNav = record.trackAddedFragmentsInPop(mTmpAddedFragments, oldPrimaryNav);
}
addToBackStack = addToBackStack || record.mAddToBackStack;
}
mTmpAddedFragments.clear();
if (!allowReordering && mCurState >= Fragment.CREATED) {
// When reordering isn't allowed, we may be operating on Fragments that haven't
// been made active
for (int index = startIndex; index < endIndex; index++) {
BackStackRecord record = records.get(index);
for (FragmentTransaction.Op op : record.mOps) {
Fragment fragment = op.mFragment;
if (fragment != null && fragment.mFragmentManager != null) {
FragmentStateManager fragmentStateManager =
createOrGetFragmentStateManager(fragment);
mFragmentStore.makeActive(fragmentStateManager);
}
}
}
}
executeOps(records, isRecordPop, startIndex, endIndex);
// The last operation determines the overall direction, this ensures that operations
// such as push, push, pop, push are correctly considered a push
boolean isPop = isRecordPop.get(endIndex - 1);
if (addToBackStack && !mBackStackChangeListeners.isEmpty()) {
Set<Fragment> fragments = new LinkedHashSet<>();
// Build a list of fragments based on the records
for (BackStackRecord record : records) {
fragments.addAll(fragmentsFromRecord(record));
}
if (mTransitioningOp == null) {
// Dispatch to all of the fragments in the list
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
// We give all fragment the back stack changed started signal first
for (Fragment fragment : fragments) {
listener.onBackStackChangeStarted(fragment, isPop);
}
}
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
// Then we give them all the committed signal
for (Fragment fragment : fragments) {
listener.onBackStackChangeCommitted(fragment, isPop);
}
}
}
}
// Ensure that Fragments directly affected by operations
// are moved to their expected state in operation order
for (int index = startIndex; index < endIndex; index++) {
BackStackRecord record = records.get(index);
if (isPop) {
// Pop operations get applied in reverse order
for (int opIndex = record.mOps.size() - 1; opIndex >= 0; opIndex--) {
FragmentTransaction.Op op = record.mOps.get(opIndex);
Fragment fragment = op.mFragment;
if (fragment != null) {
FragmentStateManager fragmentStateManager =
createOrGetFragmentStateManager(fragment);
fragmentStateManager.moveToExpectedState();
}
}
} else {
for (FragmentTransaction.Op op : record.mOps) {
Fragment fragment = op.mFragment;
if (fragment != null) {
FragmentStateManager fragmentStateManager =
createOrGetFragmentStateManager(fragment);
fragmentStateManager.moveToExpectedState();
}
}
}
}
// And only then do we move all other fragments to the current state
moveToState(mCurState, true);
Set<SpecialEffectsController> changedControllers = collectChangedControllers(
records, startIndex, endIndex);
for (SpecialEffectsController controller : changedControllers) {
controller.updateOperationDirection(isPop);
controller.markPostponedState();
controller.executePendingOperations();
}
for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
final BackStackRecord record = records.get(recordNum);
isPop = isRecordPop.get(recordNum);
if (isPop && record.mIndex >= 0) {
record.mIndex = -1;
}
record.runOnCommitRunnables();
}
if (addToBackStack) {
reportBackStackChanged();
}
}
Set<SpecialEffectsController> collectChangedControllers(
@NonNull ArrayList<BackStackRecord> records, int startIndex, int endIndex) {
Set<SpecialEffectsController> controllers = new HashSet<>();
for (int index = startIndex; index < endIndex; index++) {
BackStackRecord record = records.get(index);
for (FragmentTransaction.Op op : record.mOps) {
Fragment fragment = op.mFragment;
if (fragment != null) {
ViewGroup container = fragment.mContainer;
if (container != null) {
controllers.add(SpecialEffectsController.getOrCreateController(
container, this));
}
}
}
}
return controllers;
}
/**
* Run the operations in the BackStackRecords, either to push or pop.
*
* @param records The list of records whose operations should be run.
* @param isRecordPop The direction that these records are being run.
* @param startIndex The index of the first entry in records to run.
* @param endIndex One past the index of the final entry in records to run.
*/
private static void executeOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
for (int i = startIndex; i < endIndex; i++) {
final BackStackRecord record = records.get(i);
final boolean isPop = isRecordPop.get(i);
if (isPop) {
record.bumpBackStackNesting(-1);
record.executePopOps();
} else {
record.bumpBackStackNesting(1);
record.executeOps();
}
}
}
/**
* Set a Fragment that is visibly being removed from the screen to a tag on its container.
* If a Fragment with the same container is already set, the previously added
* Fragment has its exit animation updated to the correct exit animation (either exit or
* pop_exit).
*/
private void setVisibleRemovingFragment(@NonNull Fragment f) {
ViewGroup container = getFragmentContainer(f);
if (container != null
&& f.getEnterAnim() + f.getExitAnim() + f.getPopEnterAnim() + f.getPopExitAnim() > 0
) {
if (container.getTag(R.id.visible_removing_fragment_view_tag) == null) {
container.setTag(R.id.visible_removing_fragment_view_tag, f);
}
((Fragment) container.getTag(R.id.visible_removing_fragment_view_tag))
.setPopDirection(f.getPopDirection());
}
}
private ViewGroup getFragmentContainer(@NonNull Fragment f) {
// If there's already a container, just return it
if (f.mContainer != null) {
return f.mContainer;
}
// If the fragment has no containerId we should return null immediately.
if (f.mContainerId <= 0) {
return null;
}
// This will be false if a child fragment is added to its parent's childFragmentManager
// before a view is created for Parent. In all other cases (adding a fragment to an
// FragmentActivity's fragmentManager, adding a child fragment to a parent that has a view),
// it should be true.
if (mContainer.onHasView()) {
View view = mContainer.onFindViewById(f.mContainerId);
// We should handle the case where the container may not be a ViewGroup
if (view instanceof ViewGroup) {
return (ViewGroup) view;
}
}
return null;
}
/**
* Starts all postponed transactions regardless of whether they are ready or not.
*/
private void forcePostponedTransactions() {
Set<SpecialEffectsController> controllers = collectAllSpecialEffectsController();
for (SpecialEffectsController controller : controllers) {
controller.forcePostponedExecutePendingOperations();
}
}
/**
* Ends the animations of fragments so that they immediately reach the end state.
* This is used prior to saving the state so that the correct state is saved.
*/
private void endAnimatingAwayFragments() {
Set<SpecialEffectsController> controllers = collectAllSpecialEffectsController();
for (SpecialEffectsController controller : controllers) {
controller.forceCompleteAllOperations();
}
}
private Set<SpecialEffectsController> collectAllSpecialEffectsController() {
Set<SpecialEffectsController> controllers = new HashSet<>();
for (FragmentStateManager fragmentStateManager :
mFragmentStore.getActiveFragmentStateManagers()) {
ViewGroup container = fragmentStateManager.getFragment().mContainer;
if (container != null) {
controllers.add(SpecialEffectsController.getOrCreateController(container,
getSpecialEffectsControllerFactory()));
}
}
return controllers;
}
/**
* Adds all records in the pending actions to records and whether they are add or pop
* operations to isPop. After executing, the pending actions will be empty.
*
* @param records All pending actions will generate BackStackRecords added to this.
* This contains the transactions, in order, to execute.
* @param isPop All pending actions will generate booleans to add to this. This contains
* an entry for each entry in records to indicate whether or not it is a
* pop action.
*/
private boolean generateOpsForPendingActions(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isPop) {
boolean didSomething = false;
synchronized (mPendingActions) {
if (mPendingActions.isEmpty()) {
return false;
}
try {
final int numActions = mPendingActions.size();
for (int i = 0; i < numActions; i++) {
didSomething |= mPendingActions.get(i).generateOps(records, isPop);
}
} finally {
// Whether generateOps succeeds or not, we clear the pending actions
// to avoid re-processing the same set of actions a second time
mPendingActions.clear();
mHost.getHandler().removeCallbacks(mExecCommit);
}
}
return didSomething;
}
private void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
private void reportBackStackChanged() {
for (int i = 0; i < mBackStackChangeListeners.size(); i++) {
mBackStackChangeListeners.get(i).onBackStackChanged();
}
}
Set<Fragment> fragmentsFromRecord(@NonNull BackStackRecord record) {
Set<Fragment> fragments = new HashSet<>();
for (int i = 0; i < record.mOps.size(); i++) {
Fragment f = record.mOps.get(i).mFragment;
if (f != null && record.mAddToBackStack) {
fragments.add(f);
}
}
return fragments;
}
void addBackStackState(BackStackRecord state) {
mBackStack.add(state);
}
boolean restoreBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
BackStackState backStackState = mBackStackStates.remove(name);
if (backStackState == null) {
return false;
}
HashMap<String, Fragment> pendingSavedFragments = new HashMap<>();
for (BackStackRecord record : records) {
if (record.mBeingSaved) {
for (FragmentTransaction.Op op : record.mOps) {
if (op.mFragment != null) {
pendingSavedFragments.put(op.mFragment.mWho, op.mFragment);
}
}
}
}
List<BackStackRecord> backStackRecords = backStackState.instantiate(this,
pendingSavedFragments);
boolean added = false;
for (BackStackRecord record : backStackRecords) {
added = record.generateOps(records, isRecordPop) || added;
}
return added;
}
boolean saveBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
final int index = findBackStackIndex(name, -1, true);
if (index < 0) {
return false;
}
// Assert that all of the transactions use setReorderingAllowed(true)
// to ensure that when they are restored, they are restored as a single
// atomic operation and intermediate fragments aren't moved all the way
// up to the RESUMED state
for (int i = index; i < mBackStack.size(); i++) {
BackStackRecord record = mBackStack.get(i);
if (!record.mReorderingAllowed) {
throwException(new IllegalArgumentException("saveBackStack(\"" + name + "\") "
+ "included FragmentTransactions must use setReorderingAllowed(true) "
+ "to ensure that the back stack can be restored as an atomic operation. "
+ "Found " + record + " that did not use setReorderingAllowed(true)."));
}
}
// Assert that the set of affected fragments are entirely self contained within
// the set of transactions being saved by ensuring that the first transaction including
// that fragment includes an OP_ADD
HashSet<Fragment> allFragments = new HashSet<>();
for (int i = index; i < mBackStack.size(); i++) {
BackStackRecord record = mBackStack.get(i);
HashSet<Fragment> affectedFragments = new HashSet<>();
HashSet<Fragment> addedFragments = new HashSet<>();
for (FragmentTransaction.Op op : record.mOps) {
Fragment f = op.mFragment;
if (f == null) {
continue;
}
if (!op.mFromExpandedOp || op.mCmd == FragmentTransaction.OP_ADD
|| op.mCmd == FragmentTransaction.OP_REPLACE
|| op.mCmd == FragmentTransaction.OP_SET_PRIMARY_NAV) {
allFragments.add(f);
affectedFragments.add(f);
}
if (op.mCmd == FragmentTransaction.OP_ADD
|| op.mCmd == FragmentTransaction.OP_REPLACE) {
addedFragments.add(f);
}
}
affectedFragments.removeAll(addedFragments);
if (!affectedFragments.isEmpty()) {
throwException(new IllegalArgumentException("saveBackStack(\"" + name + "\") "
+ "must be self contained and not reference fragments from "
+ "non-saved FragmentTransactions. Found reference to fragment"
+ (affectedFragments.size() == 1
? " " + affectedFragments.iterator().next()
: "s " + affectedFragments)
+ " in " + record + " that were previously "
+ "added to the FragmentManager through a separate FragmentTransaction."));
}
}
// Ensure that there are no retained fragments in the affected fragments or
// their transitive set of child fragments
ArrayDeque<Fragment> fragmentsToSearch = new ArrayDeque<>(allFragments);
while (!fragmentsToSearch.isEmpty()) {
Fragment currentFragment = fragmentsToSearch.removeFirst();
if (currentFragment.mRetainInstance) {
throwException(new IllegalArgumentException("saveBackStack(\"" + name + "\") "
+ "must not contain retained fragments. Found "
+ (allFragments.contains(currentFragment)
? "direct reference to retained "
: "retained child ")
+ "fragment " + currentFragment));
}
// Then recursively check the child fragments for retained fragments
for (Fragment f : currentFragment.mChildFragmentManager.getActiveFragments()) {
if (f != null) {
fragmentsToSearch.addLast(f);
}
}
}
// Now actually record each save
final ArrayList<String> fragments = new ArrayList<>();
for (Fragment f : allFragments) {
fragments.add(f.mWho);
}
final ArrayList<BackStackRecordState> backStackRecordStates =
new ArrayList<>(mBackStack.size() - index);
// Add placeholders for each BackStackRecordState
for (int i = index; i < mBackStack.size(); i++) {
backStackRecordStates.add(null);
}
final BackStackState backStackState = new BackStackState(
fragments, backStackRecordStates);
for (int i = mBackStack.size() - 1; i >= index; i--) {
BackStackRecord record = mBackStack.remove(i);
// Create a copy of the record to save
BackStackRecord copy = new BackStackRecord(record);
copy.collapseOps();
BackStackRecordState state = new BackStackRecordState(copy);
backStackRecordStates.set(i - index, state);
// And now mark the record as being saved to ensure that each
// fragment saves its state properly
record.mBeingSaved = true;
records.add(record);
isRecordPop.add(true);
}
mBackStackStates.put(name, backStackState);
return true;
}
/**
* We have to handle a number of cases here:
* 1. We have no back stack state at all
* 2. We have previously saved the back stack state and we now only have the state
* 3. We are in the process of handling a saveBackStack() operation (it is in
* the set of records to be processed prior to this)
* 3a. We are in the process of handling a saveBackStack() and there are other
* FragmentTransactions queued up between that save and this clear (maybe even
* including a restoreBackStack operation).
*
* This comes together to mean that we can't actually 'clear' anything at the time
* when this particular method is called - instead, we need to enqueue exactly what
* records, etc. we need to do to get the back stack and state into the right state
* after they're all executed. This means 'clear' really means 'restore'+'pop' - as
* we 'pop' instead of 'save', any saved state (and ViewModels, etc.) will be cleared
* no matter what pending operations are enqueued up before or after this.
*/
boolean clearBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
boolean restoredBackStackState = restoreBackStackState(records, isRecordPop, name);
if (!restoredBackStackState) {
return false;
}
return popBackStackState(records, isRecordPop, name, -1, POP_BACK_STACK_INCLUSIVE);
}
@SuppressWarnings({"unused", "WeakerAccess"}) /* synthetic access */
boolean popBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, @Nullable String name, int id, int flags) {
int index = findBackStackIndex(name, id, (flags & POP_BACK_STACK_INCLUSIVE) != 0);
if (index < 0) {
return false;
}
for (int i = mBackStack.size() - 1; i >= index; i--) {
records.add(mBackStack.remove(i));
isRecordPop.add(true);
}
return true;
}
boolean prepareBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
// The transitioning record is the last one on the back stack.
mTransitioningOp = mBackStack.get(mBackStack.size() - 1);
// Mark all fragments in the record as transitioning
for (FragmentTransaction.Op op: mTransitioningOp.mOps) {
if (op.mFragment != null) {
op.mFragment.mTransitioning = true;
}
}
return popBackStackState(records, isRecordPop, null, -1, 0);
}
/**
* Find the index in the back stack associated with the given name / id.
* <p>
* When <code>inclusive</code> is <code>true</code>, the index of the matching record
* will be returned. When it is <code>false</code>, the index of the record directly
* after it will be returned. In cases where you are doing an inclusive search and
* multiple records have the same name / id, the index returned includes all
* consecutive matches following the first match.
*
* @param name The name set via {@link FragmentTransaction#addToBackStack(String)}. Use
* <code>null</code> if you do not want to search by name.
* @param id The id returned by {@link FragmentTransaction#commit()}. Use
* <code>-1</code> if you do not want to search by id.
* @param inclusive Whether to include the record specified by name or id.
* @return
*/
private int findBackStackIndex(@Nullable String name, int id, boolean inclusive) {
if (mBackStack.isEmpty()) {
return -1;
}
if (name == null && id < 0) {
if (inclusive) {
return 0;
} else {
return mBackStack.size() - 1;
}
} else {
// If a name or ID is specified, look for that place in
// the stack.
int index = mBackStack.size() - 1;
while (index >= 0) {
BackStackRecord bss = mBackStack.get(index);
if (name != null && name.equals(bss.getName())) {
break;
}
if (id >= 0 && id == bss.mIndex) {
break;
}
index--;
}
if (index < 0) {
return index;
}
if (inclusive) {
// Consume all following entries that match.
while (index > 0) {
BackStackRecord bss = mBackStack.get(index - 1);
if ((name != null && name.equals(bss.getName()))
|| (id >= 0 && id == bss.mIndex)) {
index--;
continue;
}
break;
}
} else if (index == mBackStack.size() - 1) {
// For a non-inclusive search, finding the last record
// is the same as finding nothing at all since the
// matching record itself is not included
return -1;
} else {
// Non-inclusive, so skip the actual matching record
index++;
}
return index;
}
}
/**
* @deprecated Ideally, all {@link androidx.fragment.app.FragmentHostCallback} instances
* implement ViewModelStoreOwner and we can remove this method entirely.
*/
@Deprecated
FragmentManagerNonConfig retainNonConfig() {
if (mHost instanceof ViewModelStoreOwner) {
throwException(new IllegalStateException("You cannot use retainNonConfig when your "
+ "FragmentHostCallback implements ViewModelStoreOwner."));
}
return mNonConfig.getSnapshot();
}
Parcelable saveAllState() {
if (mHost instanceof SavedStateRegistryOwner) {
throwException(new IllegalStateException("You cannot use saveAllState when your "
+ "FragmentHostCallback implements SavedStateRegistryOwner."));
}
Bundle savedState = saveAllStateInternal();
return savedState.isEmpty() ? null : savedState;
}
@NonNull
Bundle saveAllStateInternal() {
Bundle bundle = new Bundle();
// Make sure all pending operations have now been executed to get
// our state update-to-date.
forcePostponedTransactions();
endAnimatingAwayFragments();
execPendingActions(true);
mStateSaved = true;
mNonConfig.setIsStateSaved(true);
// First save all active fragments.
ArrayList<String> active = mFragmentStore.saveActiveFragments();
// And grab all fragments' saved state bundles
HashMap<String, Bundle> savedState = mFragmentStore.getAllSavedState();
if (savedState.isEmpty()) {
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "saveAllState: no fragments!");
}
} else {
// Build list of currently added fragments.
ArrayList<String> added = mFragmentStore.saveAddedFragments();
// Now save back stack.
BackStackRecordState[] backStack = null;
int size = mBackStack.size();
if (size > 0) {
backStack = new BackStackRecordState[size];
for (int i = 0; i < size; i++) {
backStack[i] = new BackStackRecordState(mBackStack.get(i));
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "saveAllState: adding back stack #" + i
+ ": " + mBackStack.get(i));
}
}
}
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
fms.mBackStackIndex = mBackStackIndex.get();
if (mPrimaryNav != null) {
fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
}
fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
fms.mBackStackStates.addAll(mBackStackStates.values());
fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
bundle.putParcelable(FRAGMENT_MANAGER_STATE_KEY, fms);
for (String resultName : mResults.keySet()) {
bundle.putBundle(RESULT_KEY_PREFIX + resultName, mResults.get(resultName));
}
for (String fWho : savedState.keySet()) {
bundle.putBundle(FRAGMENT_KEY_PREFIX + fWho, savedState.get(fWho));
}
}
return bundle;
}
@SuppressWarnings("deprecation")
void restoreAllState(@Nullable Parcelable state, @Nullable FragmentManagerNonConfig nonConfig) {
if (mHost instanceof ViewModelStoreOwner) {
throwException(new IllegalStateException("You must use restoreSaveState when your "
+ "FragmentHostCallback implements ViewModelStoreOwner"));
}
mNonConfig.restoreFromSnapshot(nonConfig);
restoreSaveStateInternal(state);
}
void restoreSaveState(@Nullable Parcelable state) {
if (mHost instanceof SavedStateRegistryOwner) {
throwException(new IllegalStateException("You cannot use restoreSaveState when your "
+ "FragmentHostCallback implements SavedStateRegistryOwner."));
}
restoreSaveStateInternal(state);
}
@SuppressWarnings("deprecation")
void restoreSaveStateInternal(@Nullable Parcelable state) {
// If there is no saved state at all, then there's nothing else to do
if (state == null) return;
Bundle bundle = (Bundle) state;
// Restore the fragment results
for (String bundleKey : bundle.keySet()) {
if (bundleKey.startsWith(RESULT_KEY_PREFIX)) {
Bundle savedResult = bundle.getBundle(bundleKey);
if (savedResult != null) {
savedResult.setClassLoader(mHost.getContext().getClassLoader());
String resultKey = bundleKey.substring(RESULT_KEY_PREFIX.length());
mResults.put(resultKey, savedResult);
}
}
}
// Restore the saved bundle for all fragments
HashMap<String, Bundle> allStateBundles = new HashMap<>();
for (String bundleKey : bundle.keySet()) {
if (bundleKey.startsWith(FRAGMENT_KEY_PREFIX)) {
Bundle savedFragmentBundle = bundle.getBundle(bundleKey);
if (savedFragmentBundle != null) {
savedFragmentBundle.setClassLoader(mHost.getContext().getClassLoader());
String fragmentKey = bundleKey.substring(FRAGMENT_KEY_PREFIX.length());
allStateBundles.put(fragmentKey, savedFragmentBundle);
}
}
}
mFragmentStore.restoreSaveState(allStateBundles);
FragmentManagerState fms = bundle.getParcelable(FRAGMENT_MANAGER_STATE_KEY);
if (fms == null) return;
// Build the full list of active fragments, instantiating them from
// their saved state.
mFragmentStore.resetActiveFragments();
for (String who : fms.mActive) {
// Retrieve any saved state, clearing it out for future calls
Bundle stateBundle = mFragmentStore.setSavedState(who, null);
if (stateBundle != null) {
FragmentStateManager fragmentStateManager;
FragmentState fs = stateBundle.getParcelable(
FragmentStateManager.FRAGMENT_STATE_KEY);
Fragment retainedFragment = mNonConfig.findRetainedFragmentByWho(fs.mWho);
if (retainedFragment != null) {
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "restoreSaveState: re-attaching retained "
+ retainedFragment);
}
fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
mFragmentStore, retainedFragment, stateBundle);
} else {
fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
mFragmentStore, mHost.getContext().getClassLoader(),
getFragmentFactory(), stateBundle);
}
Fragment f = fragmentStateManager.getFragment();
f.mSavedFragmentState = stateBundle;
f.mFragmentManager = this;
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "restoreSaveState: active (" + f.mWho + "): " + f);
}
fragmentStateManager.restoreState(mHost.getContext().getClassLoader());
mFragmentStore.makeActive(fragmentStateManager);
// Catch the FragmentStateManager up to our current state
// In almost all cases, this is Fragment.INITIALIZING, but just in
// case a FragmentController does something...unique, let's do this anyways.
fragmentStateManager.setFragmentManagerState(mCurState);
}
}
// Check to make sure there aren't any retained fragments that aren't in mActive
// This can happen if a retained fragment is added after the state is saved
for (Fragment f : mNonConfig.getRetainedFragments()) {
if (!mFragmentStore.containsActiveFragment(f.mWho)) {
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "Discarding retained Fragment " + f
+ " that was not found in the set of active Fragments " + fms.mActive);
}
mNonConfig.removeRetainedFragment(f);
// We need to ensure that onDestroy and any other clean up is done
// so move the Fragment up to CREATED, then mark it as being removed, then
// destroy it without actually adding the Fragment to the FragmentStore
f.mFragmentManager = this;
FragmentStateManager fragmentStateManager = new FragmentStateManager(
mLifecycleCallbacksDispatcher, mFragmentStore, f);
fragmentStateManager.setFragmentManagerState(Fragment.CREATED);
fragmentStateManager.moveToExpectedState();
f.mRemoving = true;
fragmentStateManager.moveToExpectedState();
}
}
// Build the list of currently added fragments.
mFragmentStore.restoreAddedFragments(fms.mAdded);
// Build the back stack.
if (fms.mBackStack != null) {
mBackStack = new ArrayList<>(fms.mBackStack.length);
for (int i = 0; i < fms.mBackStack.length; i++) {
BackStackRecord bse = fms.mBackStack[i].instantiate(this);
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "restoreAllState: back stack #" + i
+ " (index " + bse.mIndex + "): " + bse);
LogWriter logw = new LogWriter(TAG);
PrintWriter pw = new PrintWriter(logw);
bse.dump(" ", pw, false);
pw.close();
}
mBackStack.add(bse);
}
} else {
mBackStack = new ArrayList<>();
}
mBackStackIndex.set(fms.mBackStackIndex);
if (fms.mPrimaryNavActiveWho != null) {
mPrimaryNav = findActiveFragment(fms.mPrimaryNavActiveWho);
dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
}
ArrayList<String> savedBackStackStateKeys = fms.mBackStackStateKeys;
if (savedBackStackStateKeys != null) {
for (int i = 0; i < savedBackStackStateKeys.size(); i++) {
mBackStackStates.put(savedBackStackStateKeys.get(i), fms.mBackStackStates.get(i));
}
}
mLaunchedFragments = new ArrayDeque<>(fms.mLaunchedFragments);
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
@NonNull
public FragmentHostCallback<?> getHost() {
return mHost;
}
@Nullable
Fragment getParent() {
return mParent;
}
@NonNull
FragmentContainer getContainer() {
return mContainer;
}
@NonNull
FragmentStore getFragmentStore() {
return mFragmentStore;
}
@SuppressWarnings("deprecation")
void attachController(@NonNull FragmentHostCallback<?> host,
@NonNull FragmentContainer container, @Nullable final Fragment parent) {
if (mHost != null) throw new IllegalStateException("Already attached");
mHost = host;
mContainer = container;
mParent = parent;
// Add a FragmentOnAttachListener to the parent fragment / host to support
// backward compatibility with the deprecated onAttachFragment() APIs
if (mParent != null) {
addFragmentOnAttachListener(new FragmentOnAttachListener() {
@SuppressWarnings("deprecation")
@Override
public void onAttachFragment(@NonNull FragmentManager fragmentManager,
@NonNull Fragment fragment) {
parent.onAttachFragment(fragment);
}
});
} else if (host instanceof FragmentOnAttachListener) {
addFragmentOnAttachListener((FragmentOnAttachListener) host);
}
if (mParent != null) {
// Since the callback depends on us being the primary navigation fragment,
// update our callback now that we have a parent so that we have the correct
// state by default
updateOnBackPressedCallbackEnabled();
}
// Set up the OnBackPressedCallback
if (host instanceof OnBackPressedDispatcherOwner) {
OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);
mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();
LifecycleOwner owner = parent != null ? parent : dispatcherOwner;
mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);
}
// Get the FragmentManagerViewModel
if (parent != null) {
mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
} else if (host instanceof ViewModelStoreOwner) {
ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
} else {
mNonConfig = new FragmentManagerViewModel(false);
}
// Ensure that the state is in sync with FragmentManager
mNonConfig.setIsStateSaved(isStateSaved());
mFragmentStore.setNonConfig(mNonConfig);
if (mHost instanceof SavedStateRegistryOwner && parent == null) {
SavedStateRegistry registry =
((SavedStateRegistryOwner) mHost).getSavedStateRegistry();
registry.registerSavedStateProvider(SAVED_STATE_KEY, () -> {
return saveAllStateInternal();
}
);
Bundle savedInstanceState = registry
.consumeRestoredStateForKey(SAVED_STATE_KEY);
if (savedInstanceState != null) {
restoreSaveStateInternal(savedInstanceState);
}
}
if (mHost instanceof ActivityResultRegistryOwner) {
ActivityResultRegistry registry =
((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
String parentId = parent != null ? parent.mWho + ":" : "";
String keyPrefix = "FragmentManager:" + parentId;
mStartActivityForResult = registry.register(keyPrefix + "StartActivityForResult",
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
LaunchedFragmentInfo requestInfo = mLaunchedFragments.pollLast();
if (requestInfo == null) {
Log.w(TAG, "No Activities were started for result for " + this);
return;
}
String fragmentWho = requestInfo.mWho;
int requestCode = requestInfo.mRequestCode;
Fragment fragment = mFragmentStore.findFragmentByWho(fragmentWho);
// Although unlikely, it is possible this fragment could be null if a
// fragment transactions was committed immediately after the for
// result call
if (fragment == null) {
Log.w(TAG,
"Activity result delivered for unknown Fragment "
+ fragmentWho);
return;
}
fragment.onActivityResult(requestCode, result.getResultCode(),
result.getData());
}
});
mStartIntentSenderForResult = registry.register(keyPrefix
+ "StartIntentSenderForResult",
new FragmentManager.FragmentIntentSenderContract(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
LaunchedFragmentInfo requestInfo = mLaunchedFragments.pollFirst();
if (requestInfo == null) {
Log.w(TAG, "No IntentSenders were started for " + this);
return;
}
String fragmentWho = requestInfo.mWho;
int requestCode = requestInfo.mRequestCode;
Fragment fragment = mFragmentStore.findFragmentByWho(fragmentWho);
// Although unlikely, it is possible this fragment could be null if a
// fragment transactions was committed immediately after the for
// result call
if (fragment == null) {
Log.w(TAG, "Intent Sender result delivered for unknown Fragment "
+ fragmentWho);
return;
}
fragment.onActivityResult(requestCode, result.getResultCode(),
result.getData());
}
});
mRequestPermissions = registry.register(keyPrefix + "RequestPermissions",
new ActivityResultContracts.RequestMultiplePermissions(),
new ActivityResultCallback<Map<String, Boolean>>() {
@Override
public void onActivityResult(Map<String, Boolean> result) {
String[] permissions = result.keySet().toArray(new String[0]);
ArrayList<Boolean> resultValues = new ArrayList<>(result.values());
int[] grantResults = new int[resultValues.size()];
for (int i = 0; i < resultValues.size(); i++) {
grantResults[i] = resultValues.get(i)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
}
LaunchedFragmentInfo requestInfo = mLaunchedFragments.pollFirst();
if (requestInfo == null) {
Log.w(TAG, "No permissions were requested for " + this);
return;
}
String fragmentWho = requestInfo.mWho;
int requestCode = requestInfo.mRequestCode;
Fragment fragment = mFragmentStore.findFragmentByWho(fragmentWho);
// Although unlikely, it is possible this fragment could be null if a
// fragment transactions was committed immediately after the request
// permissions call
if (fragment == null) {
Log.w(TAG, "Permission request result delivered for unknown "
+ "Fragment " + fragmentWho);
return;
}
fragment.onRequestPermissionsResult(requestCode, permissions,
grantResults);
}
});
}
if (mHost instanceof OnConfigurationChangedProvider) {
OnConfigurationChangedProvider onConfigurationChangedProvider =
(OnConfigurationChangedProvider) mHost;
onConfigurationChangedProvider.addOnConfigurationChangedListener(
mOnConfigurationChangedListener);
}
if (mHost instanceof OnTrimMemoryProvider) {
OnTrimMemoryProvider onTrimMemoryProvider = (OnTrimMemoryProvider) mHost;
onTrimMemoryProvider.addOnTrimMemoryListener(mOnTrimMemoryListener);
}
if (mHost instanceof OnMultiWindowModeChangedProvider) {
OnMultiWindowModeChangedProvider onMultiWindowModeChangedProvider =
(OnMultiWindowModeChangedProvider) mHost;
onMultiWindowModeChangedProvider.addOnMultiWindowModeChangedListener(
mOnMultiWindowModeChangedListener);
}
if (mHost instanceof OnPictureInPictureModeChangedProvider) {
OnPictureInPictureModeChangedProvider onPictureInPictureModeChangedProvider =
(OnPictureInPictureModeChangedProvider) mHost;
onPictureInPictureModeChangedProvider.addOnPictureInPictureModeChangedListener(
mOnPictureInPictureModeChangedListener);
}
if (mHost instanceof MenuHost && parent == null) {
((MenuHost) mHost).addMenuProvider(mMenuProvider);
}
}
void noteStateNotSaved() {
// A fragment added via the <fragment> tag can have noteStateNotSaved() called
// by its parent fragment before attachController() has been called. In this case,
// we should early return as the state not being saved is the default.
if (mHost == null) {
return;
}
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
for (Fragment fragment : mFragmentStore.getFragments()) {
if (fragment != null) {
fragment.noteStateNotSaved();
}
}
}
void launchStartActivityForResult(@NonNull Fragment f,
@NonNull Intent intent,
int requestCode, @Nullable Bundle options) {
if (mStartActivityForResult != null) {
LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
mLaunchedFragments.addLast(info);
if (options != null) {
intent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, options);
}
mStartActivityForResult.launch(intent);
} else {
mHost.onStartActivityFromFragment(f, intent, requestCode, options);
}
}
@SuppressWarnings("deprecation")
void launchStartIntentSenderForResult(@NonNull Fragment f,
@NonNull IntentSender intent,
int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues,
int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException {
if (mStartIntentSenderForResult != null) {
if (options != null) {
if (fillInIntent == null) {
fillInIntent = new Intent();
fillInIntent.putExtra(EXTRA_CREATED_FILLIN_INTENT, true);
}
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "ActivityOptions " + options + " were added to fillInIntent "
+ fillInIntent + " for fragment " + f);
}
fillInIntent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, options);
}
IntentSenderRequest request =
new IntentSenderRequest.Builder(intent).setFillInIntent(fillInIntent)
.setFlags(flagsValues, flagsMask).build();
LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
mLaunchedFragments.addLast(info);
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "Fragment " + f + "is launching an IntentSender for result ");
}
mStartIntentSenderForResult.launch(request);
} else {
mHost.onStartIntentSenderFromFragment(f, intent, requestCode, fillInIntent,
flagsMask, flagsValues, extraFlags, options);
}
}
@SuppressWarnings("deprecation")
void launchRequestPermissions(@NonNull Fragment f, @NonNull String[] permissions,
int requestCode) {
if (mRequestPermissions != null) {
LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
mLaunchedFragments.addLast(info);
mRequestPermissions.launch(permissions);
} else {
mHost.onRequestPermissionsFromFragment(f, permissions, requestCode);
}
}
void dispatchAttach() {
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
dispatchStateChange(Fragment.ATTACHED);
}
void dispatchCreate() {
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
dispatchStateChange(Fragment.CREATED);
}
void dispatchViewCreated() {
dispatchStateChange(Fragment.VIEW_CREATED);
}
void dispatchActivityCreated() {
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
dispatchStateChange(Fragment.ACTIVITY_CREATED);
}
void dispatchStart() {
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
dispatchStateChange(Fragment.STARTED);
}
void dispatchResume() {
mStateSaved = false;
mStopped = false;
mNonConfig.setIsStateSaved(false);
dispatchStateChange(Fragment.RESUMED);
}
void dispatchPause() {
dispatchStateChange(Fragment.STARTED);
}
void dispatchStop() {
mStopped = true;
mNonConfig.setIsStateSaved(true);
dispatchStateChange(Fragment.ACTIVITY_CREATED);
}
void dispatchDestroyView() {
dispatchStateChange(Fragment.CREATED);
}
void dispatchDestroy() {
mDestroyed = true;
execPendingActions(true);
endAnimatingAwayFragments();
clearBackStackStateViewModels();
dispatchStateChange(Fragment.INITIALIZING);
if (mHost instanceof OnTrimMemoryProvider) {
OnTrimMemoryProvider onTrimMemoryProvider = (OnTrimMemoryProvider) mHost;
onTrimMemoryProvider.removeOnTrimMemoryListener(mOnTrimMemoryListener);
}
if (mHost instanceof OnConfigurationChangedProvider) {
OnConfigurationChangedProvider onConfigurationChangedProvider =
(OnConfigurationChangedProvider) mHost;
onConfigurationChangedProvider.removeOnConfigurationChangedListener(
mOnConfigurationChangedListener);
}
if (mHost instanceof OnMultiWindowModeChangedProvider) {
OnMultiWindowModeChangedProvider onMultiWindowModeChangedProvider =
(OnMultiWindowModeChangedProvider) mHost;
onMultiWindowModeChangedProvider.removeOnMultiWindowModeChangedListener(
mOnMultiWindowModeChangedListener);
}
if (mHost instanceof OnPictureInPictureModeChangedProvider) {
OnPictureInPictureModeChangedProvider onPictureInPictureModeChangedProvider =
(OnPictureInPictureModeChangedProvider) mHost;
onPictureInPictureModeChangedProvider.removeOnPictureInPictureModeChangedListener(
mOnPictureInPictureModeChangedListener);
}
if (mHost instanceof MenuHost && mParent == null) {
((MenuHost) mHost).removeMenuProvider(mMenuProvider);
}
mHost = null;
mContainer = null;
mParent = null;
if (mOnBackPressedDispatcher != null) {
// mOnBackPressedDispatcher can hold a reference to the host
// so we need to null it out to prevent memory leaks
mOnBackPressedCallback.remove();
mOnBackPressedDispatcher = null;
}
if (mStartActivityForResult != null) {
mStartActivityForResult.unregister();
mStartIntentSenderForResult.unregister();
mRequestPermissions.unregister();
}
}
private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
mFragmentStore.dispatchStateChange(nextState);
moveToState(nextState, false);
Set<SpecialEffectsController> controllers = collectAllSpecialEffectsController();
for (SpecialEffectsController controller : controllers) {
controller.forceCompleteAllOperations();
}
} finally {
mExecutingActions = false;
}
execPendingActions(true);
}
void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode, boolean recursive) {
if (recursive && mHost instanceof OnMultiWindowModeChangedProvider) {
throwException(new IllegalStateException("Do not call dispatchMultiWindowModeChanged() "
+ "on host. Host implements OnMultiWindowModeChangedProvider and automatically "
+ "dispatches multi-window mode changes to fragments."));
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
f.performMultiWindowModeChanged(isInMultiWindowMode);
if (recursive) {
f.mChildFragmentManager.dispatchMultiWindowModeChanged(
isInMultiWindowMode, true
);
}
}
}
}
void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode, boolean recursive) {
if (recursive && mHost instanceof OnPictureInPictureModeChangedProvider) {
throwException(new IllegalStateException("Do not call "
+ "dispatchPictureInPictureModeChanged() on host. Host implements "
+ "OnPictureInPictureModeChangedProvider and automatically dispatches "
+ "picture-in-picture mode changes to fragments."));
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
f.performPictureInPictureModeChanged(isInPictureInPictureMode);
if (recursive) {
f.mChildFragmentManager.dispatchPictureInPictureModeChanged(
isInPictureInPictureMode, true
);
}
}
}
}
void dispatchConfigurationChanged(@NonNull Configuration newConfig, boolean recursive) {
if (recursive && mHost instanceof OnConfigurationChangedProvider) {
throwException(new IllegalStateException("Do not call dispatchConfigurationChanged() "
+ "on host. Host implements OnConfigurationChangedProvider and automatically "
+ "dispatches configuration changes to fragments."));
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
f.performConfigurationChanged(newConfig);
if (recursive) {
f.mChildFragmentManager.dispatchConfigurationChanged(newConfig, true);
}
}
}
}
void dispatchLowMemory(boolean recursive) {
if (recursive && mHost instanceof OnTrimMemoryProvider) {
throwException(new IllegalStateException("Do not call dispatchLowMemory() on host. "
+ "Host implements OnTrimMemoryProvider and automatically dispatches "
+ "low memory callbacks to fragments."));
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
f.performLowMemory();
if (recursive) {
f.mChildFragmentManager.dispatchLowMemory(true);
}
}
}
}
@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"})
boolean dispatchCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (mCurState < Fragment.CREATED) {
return false;
}
boolean show = false;
ArrayList<Fragment> newMenus = null;
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
if (isParentMenuVisible(f) && f.performCreateOptionsMenu(menu, inflater)) {
show = true;
if (newMenus == null) {
newMenus = new ArrayList<>();
}
newMenus.add(f);
}
}
}
if (mCreatedMenus != null) {
for (int i = 0; i < mCreatedMenus.size(); i++) {
Fragment f = mCreatedMenus.get(i);
if (newMenus == null || !newMenus.contains(f)) {
f.onDestroyOptionsMenu();
}
}
}
mCreatedMenus = newMenus;
return show;
}
boolean dispatchPrepareOptionsMenu(@NonNull Menu menu) {
if (mCurState < Fragment.CREATED) {
return false;
}
boolean show = false;
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
if (isParentMenuVisible(f) && f.performPrepareOptionsMenu(menu)) {
show = true;
}
}
}
return show;
}
boolean dispatchOptionsItemSelected(@NonNull MenuItem item) {
if (mCurState < Fragment.CREATED) {
return false;
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
if (f.performOptionsItemSelected(item)) {
return true;
}
}
}
return false;
}
boolean dispatchContextItemSelected(@NonNull MenuItem item) {
if (mCurState < Fragment.CREATED) {
return false;
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
if (f.performContextItemSelected(item)) {
return true;
}
}
}
return false;
}
void dispatchOptionsMenuClosed(@NonNull Menu menu) {
if (mCurState < Fragment.CREATED) {
return;
}
for (Fragment f : mFragmentStore.getFragments()) {
if (f != null) {
f.performOptionsMenuClosed(menu);
}
}
}
void setPrimaryNavigationFragment(@Nullable Fragment f) {
if (f != null && (!f.equals(findActiveFragment(f.mWho))
|| (f.mHost != null && f.mFragmentManager != this))) {
throw new IllegalArgumentException("Fragment " + f
+ " is not an active fragment of FragmentManager " + this);
}
Fragment previousPrimaryNav = mPrimaryNav;
mPrimaryNav = f;
dispatchParentPrimaryNavigationFragmentChanged(previousPrimaryNav);
dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
}
private void dispatchParentPrimaryNavigationFragmentChanged(@Nullable Fragment f) {
if (f != null && f.equals(findActiveFragment(f.mWho))) {
f.performPrimaryNavigationFragmentChanged();
}
}
void dispatchPrimaryNavigationFragmentChanged() {
updateOnBackPressedCallbackEnabled();
// Dispatch the change event to this FragmentManager's primary navigation fragment
dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
}
/**
* Return the currently active primary navigation fragment for this FragmentManager.
* The primary navigation fragment is set by fragment transactions using
* {@link FragmentTransaction#setPrimaryNavigationFragment(Fragment)}.
*
* <p>The primary navigation fragment's
* {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first
* to process delegated navigation actions such as {@link #popBackStack()} if no ID
* or transaction name is provided to pop to.</p>
*
* @return the fragment designated as the primary navigation fragment
*/
@Nullable
public Fragment getPrimaryNavigationFragment() {
return mPrimaryNav;
}
void setMaxLifecycle(@NonNull Fragment f, @NonNull Lifecycle.State state) {
if (!f.equals(findActiveFragment(f.mWho))
|| (f.mHost != null && f.mFragmentManager != this)) {
throw new IllegalArgumentException("Fragment " + f
+ " is not an active fragment of FragmentManager " + this);
}
f.mMaxState = state;
}
/**
* Set a {@link FragmentFactory} for this FragmentManager that will be used
* to create new Fragment instances from this point onward.
* <p>
* The {@link Fragment#getChildFragmentManager() child FragmentManager} of all Fragments
* in this FragmentManager will also use this factory if one is not explicitly set.
*
* @param fragmentFactory the factory to use to create new Fragment instances
* @see #getFragmentFactory()
*/
public void setFragmentFactory(@NonNull FragmentFactory fragmentFactory) {
mFragmentFactory = fragmentFactory;
}
/**
* Gets the current {@link FragmentFactory} used to instantiate new Fragment instances.
* <p>
* If no factory has been explicitly set on this FragmentManager via
* {@link #setFragmentFactory(FragmentFactory)}, the FragmentFactory of the
* {@link Fragment#getParentFragmentManager() parent FragmentManager} will be returned.
*
* @return the current FragmentFactory
*/
@NonNull
public FragmentFactory getFragmentFactory() {
if (mFragmentFactory != null) {
return mFragmentFactory;
}
if (mParent != null) {
// This can't call setFragmentFactory since we need to
// compute this each time getFragmentFactory() is called
// so that if the parent's FragmentFactory changes, we
// pick the change up here.
return mParent.mFragmentManager.getFragmentFactory();
}
return mHostFragmentFactory;
}
/**
* Set a {@link SpecialEffectsControllerFactory} for this FragmentManager that will be used
* to create new SpecialEffectsController instances from this point onward.
*
* @param specialEffectsControllerFactory the factory to use to create new
* SpecialEffectsController instances.
*/
void setSpecialEffectsControllerFactory(
@NonNull SpecialEffectsControllerFactory specialEffectsControllerFactory) {
mSpecialEffectsControllerFactory = specialEffectsControllerFactory;
}
/**
* Gets the current {@link SpecialEffectsControllerFactory} used to instantiate new
* SpecialEffectsController instances.
*
* @return the current SpecialEffectsControllerFactory
*/
@NonNull
SpecialEffectsControllerFactory getSpecialEffectsControllerFactory() {
if (mSpecialEffectsControllerFactory != null) {
return mSpecialEffectsControllerFactory;
}
if (mParent != null) {
// This can't call setSpecialEffectsControllerFactory since we need to
// compute this each time getSpecialEffectsControllerFactory() is called
// so that if the parent's SpecialEffectsControllerFactory changes, we
// pick the change up here.
return mParent.mFragmentManager.getSpecialEffectsControllerFactory();
}
return mDefaultSpecialEffectsControllerFactory;
}
@NonNull
FragmentLifecycleCallbacksDispatcher getLifecycleCallbacksDispatcher() {
return mLifecycleCallbacksDispatcher;
}
/**
* Registers a {@link FragmentLifecycleCallbacks} to listen to fragment lifecycle events
* happening in this FragmentManager. All registered callbacks will be automatically
* unregistered when this FragmentManager is destroyed.
*
* @param cb Callbacks to register
* @param recursive true to automatically register this callback for all child FragmentManagers
*/
public void registerFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb,
boolean recursive) {
mLifecycleCallbacksDispatcher.registerFragmentLifecycleCallbacks(cb, recursive);
}
/**
* Unregisters a previously registered {@link FragmentLifecycleCallbacks}. If the callback
* was not previously registered this call has no effect. All registered callbacks will be
* automatically unregistered when this FragmentManager is destroyed.
*
* @param cb Callbacks to unregister
*/
public void unregisterFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb) {
mLifecycleCallbacksDispatcher.unregisterFragmentLifecycleCallbacks(cb);
}
/**
* Add a {@link FragmentOnAttachListener} that should receive a call to
* {@link FragmentOnAttachListener#onAttachFragment(FragmentManager, Fragment)} when a
* new Fragment is attached to this FragmentManager.
*
* @param listener Listener to add
*/
public void addFragmentOnAttachListener(@NonNull FragmentOnAttachListener listener) {
mOnAttachListeners.add(listener);
}
/**
* Dispatch {@link FragmentOnAttachListener#onAttachFragment(FragmentManager, Fragment)} to
* each listener registered via {@link #addFragmentOnAttachListener(FragmentOnAttachListener)}.
*
* @param fragment The Fragment that was attached
*/
void dispatchOnAttachFragment(@NonNull Fragment fragment) {
for (FragmentOnAttachListener listener : mOnAttachListeners) {
listener.onAttachFragment(this, fragment);
}
}
/**
* Remove a {@link FragmentOnAttachListener} that was previously added via
* {@link #addFragmentOnAttachListener(FragmentOnAttachListener)}. It will no longer
* get called when a new Fragment is attached.
*
* @param listener Listener to remove
*/
public void removeFragmentOnAttachListener(@NonNull FragmentOnAttachListener listener) {
mOnAttachListeners.remove(listener);
}
void dispatchOnHiddenChanged() {
for (Fragment fragment : mFragmentStore.getActiveFragments()) {
if (fragment != null) {
fragment.onHiddenChanged(fragment.isHidden());
fragment.mChildFragmentManager.dispatchOnHiddenChanged();
}
}
}
// Checks if fragments that belong to this fragment manager (or their children) have menus,
// and if they are visible.
boolean checkForMenus() {
boolean hasMenu = false;
for (Fragment fragment : mFragmentStore.getActiveFragments()) {
if (fragment != null) {
hasMenu = isMenuAvailable(fragment);
}
if (hasMenu) {
return true;
}
}
return false;
}
private boolean isMenuAvailable(@NonNull Fragment f) {
return (f.mHasMenu && f.mMenuVisible) || f.mChildFragmentManager.checkForMenus();
}
void invalidateMenuForFragment(@NonNull Fragment f) {
if (f.mAdded && isMenuAvailable(f)) {
mNeedMenuInvalidate = true;
}
}
private boolean isParentAdded() {
// The root fragment manager is always considered added
if (mParent == null) {
return true;
}
return mParent.isAdded() && mParent.getParentFragmentManager().isParentAdded();
}
static int reverseTransit(int transit) {
int rev = 0;
switch (transit) {
case FragmentTransaction.TRANSIT_FRAGMENT_OPEN:
rev = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;
break;
case FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:
rev = FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
break;
case FragmentTransaction.TRANSIT_FRAGMENT_FADE:
rev = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
break;
case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN:
rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE;
break;
case FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_CLOSE:
rev = FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN;
break;
}
return rev;
}
@NonNull
LayoutInflater.Factory2 getLayoutInflaterFactory() {
return mLayoutInflaterFactory;
}
/** Returns the current policy for this FragmentManager. If no policy is set, returns null. */
@Nullable
public FragmentStrictMode.Policy getStrictModePolicy() {
return mStrictModePolicy;
}
/**
* Sets the policy for what actions should be detected, as well as the penalty if such actions
* occur. The {@link Fragment#getChildFragmentManager() child FragmentManager} of all Fragments
* in this FragmentManager will also use this policy if one is not explicitly set. Pass null to
* clear the policy.
*
* @param policy the policy to put into place
*/
public void setStrictModePolicy(@Nullable FragmentStrictMode.Policy policy) {
mStrictModePolicy = policy;
}
/**
* An add or pop transaction to be scheduled for the UI thread.
*/
interface OpGenerator {
/**
* Generate transactions to add to {@code records} and whether or not the transaction is
* an add or pop to {@code isRecordPop}.
*
* records and isRecordPop must be added equally so that each transaction in records
* matches the boolean for whether or not it is a pop in isRecordPop.
*
* @param records A list to add transactions to.
* @param isRecordPop A list to add whether or not the transactions added to records is
* a pop transaction.
* @return true if something was added or false otherwise.
*/
boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop);
}
/**
* A pop operation OpGenerator. This will be run on the UI thread and will generate the
* transactions that will be popped if anything can be popped.
*/
private class PopBackStackState implements OpGenerator {
final String mName;
final int mId;
final int mFlags;
PopBackStackState(@Nullable String name, int id, int flags) {
mName = name;
mId = id;
mFlags = flags;
}
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
if (mPrimaryNav != null // We have a primary nav fragment
&& mId < 0 // No valid id (since they're local)
&& mName == null) { // no name to pop to (since they're local)
final FragmentManager childManager = mPrimaryNav.getChildFragmentManager();
if (childManager.popBackStackImmediate()) {
// We didn't add any operations for this FragmentManager even though
// a child did do work.
return false;
}
}
return popBackStackState(records, isRecordPop, mName, mId, mFlags);
}
}
private class RestoreBackStackState implements OpGenerator {
private final String mName;
RestoreBackStackState(@NonNull String name) {
mName = name;
}
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
return restoreBackStackState(records, isRecordPop, mName);
}
}
private class SaveBackStackState implements OpGenerator {
private final String mName;
SaveBackStackState(@NonNull String name) {
mName = name;
}
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
return saveBackStackState(records, isRecordPop, mName);
}
}
private class ClearBackStackState implements OpGenerator {
private final String mName;
ClearBackStackState(@NonNull String name) {
mName = name;
}
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
return clearBackStackState(records, isRecordPop, mName);
}
}
class PrepareBackStackTransitionState implements OpGenerator {
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
boolean result = prepareBackStackState(records, isRecordPop);
mBackStarted = true;
// Dispatch started signal to onBackStackChangedListeners.
if (!mBackStackChangeListeners.isEmpty()) {
if (records.size() > 0) {
boolean isPop = isRecordPop.get(records.size() - 1);
Set<Fragment> fragments = new LinkedHashSet<>();
// Build a list of fragments based on the records
for (BackStackRecord record : records) {
fragments.addAll(fragmentsFromRecord(record));
}
// Dispatch to all of the fragments in the list
for (OnBackStackChangedListener listener : mBackStackChangeListeners) {
// We give all fragment the back stack changed started signal first
for (Fragment fragment : fragments) {
listener.onBackStackChangeStarted(fragment, isPop);
}
}
}
}
return result;
}
}
@SuppressLint("BanParcelableUsage")
static class LaunchedFragmentInfo implements Parcelable {
String mWho;
int mRequestCode;
LaunchedFragmentInfo(@NonNull String who, int requestCode) {
mWho = who;
mRequestCode = requestCode;
}
LaunchedFragmentInfo(@NonNull Parcel in) {
mWho = in.readString();
mRequestCode = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mWho);
dest.writeInt(mRequestCode);
}
public static final Parcelable.Creator<LaunchedFragmentInfo> CREATOR =
new Creator<LaunchedFragmentInfo>() {
@Override
public LaunchedFragmentInfo createFromParcel(Parcel in) {
return new LaunchedFragmentInfo(in);
}
@Override
public LaunchedFragmentInfo[] newArray(int size) {
return new LaunchedFragmentInfo[size];
}
};
}
static class FragmentIntentSenderContract extends ActivityResultContract<IntentSenderRequest,
ActivityResult> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, IntentSenderRequest input) {
Intent result = new Intent(ACTION_INTENT_SENDER_REQUEST);
Intent fillInIntent = input.getFillInIntent();
if (fillInIntent != null) {
Bundle activityOptions = fillInIntent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
if (activityOptions != null) {
result.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, activityOptions);
fillInIntent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
if (fillInIntent.getBooleanExtra(EXTRA_CREATED_FILLIN_INTENT, false)) {
input = new IntentSenderRequest.Builder(input.getIntentSender())
.setFillInIntent(null)
.setFlags(input.getFlagsValues(), input.getFlagsMask())
.build();
}
}
}
result.putExtra(EXTRA_INTENT_SENDER_REQUEST, input);
if (isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "CreateIntent created the following intent: " + result);
}
return result;
}
@NonNull
@Override
public ActivityResult parseResult(int resultCode, @Nullable Intent intent) {
return new ActivityResult(resultCode, intent);
}
}
}