content: /*
   * Copyright (C) 2016 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 android.app;
  
  import android.graphics.Rect;
  import android.os.Build;
  import android.transition.Transition;
  import android.transition.TransitionListenerAdapter;
  import android.transition.TransitionManager;
  import android.transition.TransitionSet;
  import android.util.ArrayMap;
  import android.util.SparseArray;
  import android.view.View;
  import android.view.ViewGroup;
  
  import com.android.internal.view.OneShotPreDrawListener;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Contains the Fragment Transition functionality for both ordered and reordered
   * Fragment Transactions. With reordered fragment transactions, all Views have been
   * added to the View hierarchy prior to calling startTransitions. With ordered
   * fragment transactions, Views will be removed and added after calling startTransitions.
   */
  class FragmentTransition {
      /**
       * The inverse of all BackStackRecord operation commands. This assumes that
       * REPLACE operations have already been replaced by add/remove operations.
       */
      private static final int[] INVERSE_OPS = {
              BackStackRecord.OP_NULL,              // inverse of OP_NULL (error)
              BackStackRecord.OP_REMOVE,            // inverse of OP_ADD
              BackStackRecord.OP_NULL,              // inverse of OP_REPLACE (error)
              BackStackRecord.OP_ADD,               // inverse of OP_REMOVE
              BackStackRecord.OP_SHOW,              // inverse of OP_HIDE
              BackStackRecord.OP_HIDE,              // inverse of OP_SHOW
              BackStackRecord.OP_ATTACH,            // inverse of OP_DETACH
              BackStackRecord.OP_DETACH,            // inverse of OP_ATTACH
              BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
              BackStackRecord.OP_SET_PRIMARY_NAV,   // inverse of OP_UNSET_PRIMARY_NAV
      };
  
      /**
       * The main entry point for Fragment Transitions, this starts the transitions
       * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the
       * entering Fragment's {@link Fragment#getEnterTransition()} and
       * {@link Fragment#getSharedElementEnterTransition()}. When popping,
       * the leaving Fragment's {@link Fragment#getReturnTransition()} and
       * {@link Fragment#getSharedElementReturnTransition()} and the entering
       * {@link Fragment#getReenterTransition()} will be run.
       * <p>
       * With reordered Fragment Transitions, all Views have been added to the
       * View hierarchy prior to calling this method. The incoming Fragment's Views
       * will be INVISIBLE. With ordered Fragment Transitions, this method
       * is called before any change has been made to the hierarchy. That means
       * that the added Fragments have not created their Views yet and the hierarchy
       * is unknown.
       *
       * @param fragmentManager The executing FragmentManagerImpl
       * @param records The list of transactions being executed.
       * @param isRecordPop For each transaction, whether it is a pop transaction or not.
       * @param startIndex The first index into records and isRecordPop to execute as
       *                   part of this transition.
       * @param endIndex One past the last index into records and isRecordPop to execute
       *                 as part of this transition.
       * @param isReordered true if this is a reordered transaction, meaning that the
       *                    Views of incoming fragments have been added. false if the
       *                    transaction has yet to be run and Views haven't been created.
       */
      static void startTransitions(FragmentManagerImpl fragmentManager,
              ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
              int startIndex, int endIndex, boolean isReordered) {
          if (fragmentManager.mCurState < Fragment.CREATED) {
              return;
          }
          SparseArray<FragmentContainerTransition> transitioningFragments =
                  new SparseArray<>();
          for (int i = startIndex; i < endIndex; i++) {
              final BackStackRecord record = records.get(i);
              final boolean isPop = isRecordPop.get(i);
              if (isPop) {
                  calculatePopFragments(record, transitioningFragments, isReordered);
              } else {
                  calculateFragments(record, transitioningFragments, isReordered);
              }
          }
  
          if (transitioningFragments.size() != 0) {
              final View nonExistentView = new View(fragmentManager.mHost.getContext());
              final int numContainers = transitioningFragments.size();
              for (int i = 0; i < numContainers; i++) {
                  int containerId = transitioningFragments.keyAt(i);
                  ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,
                          records, isRecordPop, startIndex, endIndex);
  
                  FragmentContainerTransition containerTransition = transitioningFragments.valueAt(i);
  
                  if (isReordered) {
                      configureTransitionsReordered(fragmentManager, containerId,
                              containerTransition, nonExistentView, nameOverrides);
                  } else {
                      configureTransitionsOrdered(fragmentManager, containerId,
                              containerTransition, nonExistentView, nameOverrides);
                  }
              }
          }
      }
  
      /**
       * Iterates through the transactions that affect a given fragment container
       * and tracks the shared element names across transactions. This is most useful
       * in pop transactions where the names of shared elements are known.
       *
       * @param containerId The container ID that is executing the transition.
       * @param records The list of transactions being executed.
       * @param isRecordPop For each transaction, whether it is a pop transaction or not.
       * @param startIndex The first index into records and isRecordPop to execute as
       *                   part of this transition.
       * @param endIndex One past the last index into records and isRecordPop to execute
       *                 as part of this transition.
       * @return A map from the initial shared element name to the final shared element name
       * before any onMapSharedElements is run.
       */
      private static ArrayMap<String, String> calculateNameOverrides(int containerId,
              ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
              int startIndex, int endIndex) {
          ArrayMap<String, String> nameOverrides = new ArrayMap<>();
          for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) {
              final BackStackRecord record = records.get(recordNum);
              if (!record.interactsWith(containerId)) {
                  continue;
              }
              final boolean isPop = isRecordPop.get(recordNum);
              if (record.mSharedElementSourceNames != null) {
                  final int numSharedElements = record.mSharedElementSourceNames.size();
                  final ArrayList<String> sources;
                  final ArrayList<String> targets;
                  if (isPop) {
                      targets = record.mSharedElementSourceNames;
                      sources = record.mSharedElementTargetNames;
                  } else {
                      sources = record.mSharedElementSourceNames;
                      targets = record.mSharedElementTargetNames;
                  }
                  for (int i = 0; i < numSharedElements; i++) {
                      String sourceName = sources.get(i);
                      String targetName = targets.get(i);
                      String previousTarget = nameOverrides.remove(targetName);
                      if (previousTarget != null) {
                          nameOverrides.put(sourceName, previousTarget);
                      } else {
                          nameOverrides.put(sourceName, targetName);
                      }
                  }
              }
          }
          return nameOverrides;
      }
  
      /**
       * Configures a transition for a single fragment container for which the transaction was
       * reordered. That means that all Fragment Views have been added and incoming fragment
       * Views are marked invisible.
       *
       * @param fragmentManager The executing FragmentManagerImpl
       * @param containerId The container ID that is executing the transition.
       * @param fragments A structure holding the transitioning fragments in this container.
       * @param nonExistentView A View that does not exist in the hierarchy. This is used to
       *                        prevent transitions from acting on other Views when there is no
       *                        other target.
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       */
      private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,
              int containerId, FragmentContainerTransition fragments,
              View nonExistentView, ArrayMap<String, String> nameOverrides) {
          ViewGroup sceneRoot = null;
          if (fragmentManager.mContainer.onHasView()) {
              sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
          }
          if (sceneRoot == null) {
              return;
          }
          final Fragment inFragment = fragments.lastIn;
          final Fragment outFragment = fragments.firstOut;
          final boolean inIsPop = fragments.lastInIsPop;
          final boolean outIsPop = fragments.firstOutIsPop;
  
          ArrayList<View> sharedElementsIn = new ArrayList<>();
          ArrayList<View> sharedElementsOut = new ArrayList<>();
          Transition enterTransition = getEnterTransition(inFragment, inIsPop);
          Transition exitTransition = getExitTransition(outFragment, outIsPop);
  
          TransitionSet sharedElementTransition = configureSharedElementsReordered(sceneRoot,
                  nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
                  enterTransition, exitTransition);
  
          if (enterTransition == null && sharedElementTransition == null &&
                  exitTransition == null) {
              return; // no transitions!
          }
  
          ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
                  outFragment, sharedElementsOut, nonExistentView);
  
          ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition,
                  inFragment, sharedElementsIn, nonExistentView);
  
          setViewVisibility(enteringViews, View.INVISIBLE);
  
          Transition transition = mergeTransitions(enterTransition, exitTransition,
                  sharedElementTransition, inFragment, inIsPop);
  
          if (transition != null) {
              replaceHide(exitTransition, outFragment, exitingViews);
              transition.setNameOverrides(nameOverrides);
              scheduleRemoveTargets(transition,
                      enterTransition, enteringViews, exitTransition, exitingViews,
                      sharedElementTransition, sharedElementsIn);
              TransitionManager.beginDelayedTransition(sceneRoot, transition);
              setViewVisibility(enteringViews, View.VISIBLE);
              // Swap the shared element targets
              if (sharedElementTransition != null) {
                  sharedElementTransition.getTargets().clear();
                  sharedElementTransition.getTargets().addAll(sharedElementsIn);
                  replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
              }
          }
      }
  
      /**
       * Configures a transition for a single fragment container for which the transaction was
       * ordered. That means that the transaction has not been executed yet, so incoming
       * Views are not yet known.
       *
       * @param fragmentManager The executing FragmentManagerImpl
       * @param containerId The container ID that is executing the transition.
       * @param fragments A structure holding the transitioning fragments in this container.
       * @param nonExistentView A View that does not exist in the hierarchy. This is used to
       *                        prevent transitions from acting on other Views when there is no
       *                        other target.
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       */
      private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,
              int containerId, FragmentContainerTransition fragments,
              View nonExistentView, ArrayMap<String, String> nameOverrides) {
          ViewGroup sceneRoot = null;
          if (fragmentManager.mContainer.onHasView()) {
              sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
          }
          if (sceneRoot == null) {
              return;
          }
          final Fragment inFragment = fragments.lastIn;
          final Fragment outFragment = fragments.firstOut;
          final boolean inIsPop = fragments.lastInIsPop;
          final boolean outIsPop = fragments.firstOutIsPop;
  
          Transition enterTransition = getEnterTransition(inFragment, inIsPop);
          Transition exitTransition = getExitTransition(outFragment, outIsPop);
  
          ArrayList<View> sharedElementsOut = new ArrayList<>();
          ArrayList<View> sharedElementsIn = new ArrayList<>();
  
          TransitionSet sharedElementTransition = configureSharedElementsOrdered(sceneRoot,
                  nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
                  enterTransition, exitTransition);
  
          if (enterTransition == null && sharedElementTransition == null &&
                  exitTransition == null) {
              return; // no transitions!
          }
  
          ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
                  outFragment, sharedElementsOut, nonExistentView);
  
          if (exitingViews == null || exitingViews.isEmpty()) {
              exitTransition = null;
          }
  
          if (enterTransition != null) {
              // Ensure the entering transition doesn't target anything until the views are made
              // visible
              enterTransition.addTarget(nonExistentView);
          }
  
          Transition transition = mergeTransitions(enterTransition, exitTransition,
                  sharedElementTransition, inFragment, fragments.lastInIsPop);
  
          if (transition != null) {
              transition.setNameOverrides(nameOverrides);
              final ArrayList<View> enteringViews = new ArrayList<>();
              scheduleRemoveTargets(transition,
                      enterTransition, enteringViews, exitTransition, exitingViews,
                      sharedElementTransition, sharedElementsIn);
              scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn,
                      enterTransition, enteringViews, exitTransition, exitingViews);
  
              TransitionManager.beginDelayedTransition(sceneRoot, transition);
          }
      }
  
      /**
       * Replace hide operations with visibility changes on the exiting views. Instead of making
       * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the
       * transition, make the fragment's view GONE.
       */
      private static void replaceHide(Transition exitTransition, Fragment exitingFragment,
              final ArrayList<View> exitingViews) {
          if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded
                  && exitingFragment.mHidden && exitingFragment.mHiddenChanged) {
              exitingFragment.setHideReplaced(true);
              final View fragmentView = exitingFragment.getView();
              OneShotPreDrawListener.add(exitingFragment.mContainer, () -> {
                  setViewVisibility(exitingViews, View.INVISIBLE);
              });
              exitTransition.addListener(new TransitionListenerAdapter() {
                  @Override
                  public void onTransitionEnd(Transition transition) {
                      transition.removeListener(this);
                      fragmentView.setVisibility(View.GONE);
                      setViewVisibility(exitingViews, View.VISIBLE);
                  }
              });
          }
      }
  
      /**
       * This method is used for fragment transitions for ordered transactions to change the
       * enter and exit transition targets after the call to
       * {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. The exit transition
       * must ensure that it does not target any Views and the enter transition must start targeting
       * the Views of the incoming Fragment.
       *
       * @param sceneRoot The fragment container View
       * @param inFragment The last fragment that is entering
       * @param nonExistentView A view that does not exist in the hierarchy that is used as a
       *                        transition target to ensure no View is targeted.
       * @param sharedElementsIn The shared element Views of the incoming fragment
       * @param enterTransition The enter transition of the incoming fragment
       * @param enteringViews The entering Views of the incoming fragment
       * @param exitTransition The exit transition of the outgoing fragment
       * @param exitingViews The exiting views of the outgoing fragment
       */
      private static void scheduleTargetChange(final ViewGroup sceneRoot,
              final Fragment inFragment, final View nonExistentView,
              final ArrayList<View> sharedElementsIn,
              final Transition enterTransition, final ArrayList<View> enteringViews,
              final Transition exitTransition, final ArrayList<View> exitingViews) {
  
          OneShotPreDrawListener.add(sceneRoot, () -> {
              if (enterTransition != null) {
                  enterTransition.removeTarget(nonExistentView);
                  ArrayList<View> views = configureEnteringExitingViews(
                          enterTransition, inFragment, sharedElementsIn, nonExistentView);
                  enteringViews.addAll(views);
              }
  
              if (exitingViews != null) {
                  if (exitTransition != null) {
                      ArrayList<View> tempExiting = new ArrayList<>();
                      tempExiting.add(nonExistentView);
                      replaceTargets(exitTransition, exitingViews, tempExiting);
                  }
                  exitingViews.clear();
                  exitingViews.add(nonExistentView);
              }
          });
      }
  
      /**
       * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet
       * targets all shared elements to ensure that no other Views are targeted. The shared element
       * transition can then target any or all shared elements without worrying about accidentally
       * targeting entering or exiting Views.
       *
       * @param inFragment The incoming fragment
       * @param outFragment the outgoing fragment
       * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.
       * @return A TransitionSet wrapping the shared element transition or null if no such transition
       * exists.
       */
      private static TransitionSet getSharedElementTransition(Fragment inFragment,
              Fragment outFragment, boolean isPop) {
          if (inFragment == null || outFragment == null) {
              return null;
          }
          Transition transition = cloneTransition(isPop
                  ? outFragment.getSharedElementReturnTransition()
                  : inFragment.getSharedElementEnterTransition());
          if (transition == null) {
              return null;
          }
          TransitionSet transitionSet = new TransitionSet();
          transitionSet.addTransition(transition);
          return transitionSet;
      }
  
      /**
       * Returns a clone of the enter transition or null if no such transition exists.
       */
      private static Transition getEnterTransition(Fragment inFragment, boolean isPop) {
          if (inFragment == null) {
              return null;
          }
          return cloneTransition(isPop ? inFragment.getReenterTransition() :
                  inFragment.getEnterTransition());
      }
  
      /**
       * Returns a clone of the exit transition or null if no such transition exists.
       */
      private static Transition getExitTransition(Fragment outFragment, boolean isPop) {
          if (outFragment == null) {
              return null;
          }
          return cloneTransition(isPop ? outFragment.getReturnTransition() :
                  outFragment.getExitTransition());
      }
  
      /**
       * Returns a clone of a transition or null if it is null
       */
      private static Transition cloneTransition(Transition transition) {
          if (transition != null) {
              transition = transition.clone();
          }
          return transition;
      }
  
      /**
       * Configures the shared elements of an reordered fragment transaction's transition.
       * This retrieves the shared elements of the outgoing and incoming fragments, maps the
       * views, and sets up the epicenter on the transitions.
       * <p>
       * The epicenter of exit and shared element transitions is the first shared element
       * in the outgoing fragment. The epicenter of the entering transition is the first shared
       * element in the incoming fragment.
       *
       * @param sceneRoot The fragment container View
       * @param nonExistentView A View that does not exist in the hierarchy. This is used to
       *                        prevent transitions from acting on other Views when there is no
       *                        other target.
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       * @param fragments A structure holding the transitioning fragments in this container.
       * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
       *                          fragment
       * @param sharedElementsIn A list modified to contain the shared elements in the incoming
       *                         fragment
       * @param enterTransition The transition used for entering Views, modified by applying the
       *                        epicenter
       * @param exitTransition The transition used for exiting Views, modified by applying the
       *                       epicenter
       * @return The shared element transition or null if no shared elements exist
       */
      private static TransitionSet configureSharedElementsReordered(final ViewGroup sceneRoot,
              final View nonExistentView, ArrayMap<String, String> nameOverrides,
              final FragmentContainerTransition fragments,
              final ArrayList<View> sharedElementsOut,
              final ArrayList<View> sharedElementsIn,
              final Transition enterTransition, final Transition exitTransition) {
          final Fragment inFragment = fragments.lastIn;
          final Fragment outFragment = fragments.firstOut;
          if (inFragment != null) {
              inFragment.getView().setVisibility(View.VISIBLE);
          }
          if (inFragment == null || outFragment == null) {
              return null; // no shared element without a fragment
          }
  
          final boolean inIsPop = fragments.lastInIsPop;
          TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
                  : getSharedElementTransition(inFragment, outFragment, inIsPop);
  
          ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
                  sharedElementTransition, fragments);
  
          ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides,
                  sharedElementTransition, fragments);
  
          if (nameOverrides.isEmpty()) {
              sharedElementTransition = null;
              if (outSharedElements != null) {
                  outSharedElements.clear();
              }
              if (inSharedElements != null) {
                  inSharedElements.clear();
              }
          } else {
              addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,
                      nameOverrides.keySet());
              addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,
                      nameOverrides.values());
          }
  
          if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
              // don't call onSharedElementStart/End since there is no transition
              return null;
          }
  
          callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
  
          final Rect epicenter;
          final View epicenterView;
          if (sharedElementTransition != null) {
              sharedElementsIn.add(nonExistentView);
              setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
              final boolean outIsPop = fragments.firstOutIsPop;
              final BackStackRecord outTransaction = fragments.firstOutTransaction;
              setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
                      outTransaction);
              epicenter = new Rect();
              epicenterView = getInEpicenterView(inSharedElements, fragments,
                      enterTransition, inIsPop);
              if (epicenterView != null) {
                  enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
                      @Override
                      public Rect onGetEpicenter(Transition transition) {
                          return epicenter;
                      }
                  });
              }
          } else {
              epicenter = null;
              epicenterView = null;
          }
  
          OneShotPreDrawListener.add(sceneRoot, () -> {
              callSharedElementStartEnd(inFragment, outFragment, inIsPop,
                      inSharedElements, false);
              if (epicenterView != null) {
                  epicenterView.getBoundsOnScreen(epicenter);
              }
          });
          return sharedElementTransition;
      }
  
      /**
       * Add Views from sharedElements into views that have the transitionName in the
       * nameOverridesSet.
       *
       * @param views               Views list to add shared elements to
       * @param sharedElements      List of shared elements
       * @param nameOverridesSet    The transition names for all views to be copied from
       *                            sharedElements to views.
       */
      private static void addSharedElementsWithMatchingNames(ArrayList<View> views,
              ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {
          for (int i = sharedElements.size() - 1; i >= 0; i--) {
              View view = sharedElements.valueAt(i);
              if (view != null && nameOverridesSet.contains(view.getTransitionName())) {
                  views.add(view);
              }
          }
      }
  
      /**
       * Configures the shared elements of an ordered fragment transaction's transition.
       * This retrieves the shared elements of the incoming fragments, and schedules capturing
       * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter
       * on the transitions.
       * <p>
       * The epicenter of exit and shared element transitions is the first shared element
       * in the outgoing fragment. The epicenter of the entering transition is the first shared
       * element in the incoming fragment.
       *
       * @param sceneRoot The fragment container View
       * @param nonExistentView A View that does not exist in the hierarchy. This is used to
       *                        prevent transitions from acting on other Views when there is no
       *                        other target.
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       * @param fragments A structure holding the transitioning fragments in this container.
       * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
       *                          fragment
       * @param sharedElementsIn A list modified to contain the shared elements in the incoming
       *                         fragment
       * @param enterTransition The transition used for entering Views, modified by applying the
       *                        epicenter
       * @param exitTransition The transition used for exiting Views, modified by applying the
       *                       epicenter
       * @return The shared element transition or null if no shared elements exist
       */
      private static TransitionSet configureSharedElementsOrdered(final ViewGroup sceneRoot,
              final View nonExistentView, ArrayMap<String, String> nameOverrides,
              final FragmentContainerTransition fragments,
              final ArrayList<View> sharedElementsOut,
              final ArrayList<View> sharedElementsIn,
              final Transition enterTransition, final Transition exitTransition) {
          final Fragment inFragment = fragments.lastIn;
          final Fragment outFragment = fragments.firstOut;
  
          if (inFragment == null || outFragment == null) {
              return null; // no transition
          }
  
          final boolean inIsPop = fragments.lastInIsPop;
          TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
                  : getSharedElementTransition(inFragment, outFragment, inIsPop);
  
          ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
                  sharedElementTransition, fragments);
  
          if (nameOverrides.isEmpty()) {
              sharedElementTransition = null;
          } else {
              sharedElementsOut.addAll(outSharedElements.values());
          }
  
          if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
              // don't call onSharedElementStart/End since there is no transition
              return null;
          }
  
          callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
  
          final Rect inEpicenter;
          if (sharedElementTransition != null) {
              inEpicenter = new Rect();
              setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
              final boolean outIsPop = fragments.firstOutIsPop;
              final BackStackRecord outTransaction = fragments.firstOutTransaction;
              setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
                      outTransaction);
              if (enterTransition != null) {
                  enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
                      @Override
                      public Rect onGetEpicenter(Transition transition) {
                          if (inEpicenter.isEmpty()) {
                              return null;
                          }
                          return inEpicenter;
                      }
                  });
              }
          } else {
              inEpicenter = null;
          }
  
          TransitionSet finalSharedElementTransition = sharedElementTransition;
  
          OneShotPreDrawListener.add(sceneRoot, () -> {
              ArrayMap<String, View> inSharedElements = captureInSharedElements(
                      nameOverrides, finalSharedElementTransition, fragments);
  
              if (inSharedElements != null) {
                  sharedElementsIn.addAll(inSharedElements.values());
                  sharedElementsIn.add(nonExistentView);
              }
  
              callSharedElementStartEnd(inFragment, outFragment, inIsPop,
                      inSharedElements, false);
              if (finalSharedElementTransition != null) {
                  finalSharedElementTransition.getTargets().clear();
                  finalSharedElementTransition.getTargets().addAll(sharedElementsIn);
                  replaceTargets(finalSharedElementTransition, sharedElementsOut,
                          sharedElementsIn);
  
                  final View inEpicenterView = getInEpicenterView(inSharedElements,
                          fragments, enterTransition, inIsPop);
                  if (inEpicenterView != null) {
                      inEpicenterView.getBoundsOnScreen(inEpicenter);
                  }
              }
          });
          return sharedElementTransition;
      }
  
      /**
       * Finds the shared elements in the outgoing fragment. It also calls
       * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
       * of the shared element mapping. {@code nameOverrides} is updated to match the
       * actual transition name of the mapped shared elements.
       *
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       * @param sharedElementTransition The shared element transition
       * @param fragments A structure holding the transitioning fragments in this container.
       * @return The mapping of shared element names to the Views in the hierarchy or null
       * if there is no shared element transition.
       */
      private static ArrayMap<String, View> captureOutSharedElements(
              ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
              FragmentContainerTransition fragments) {
          if (nameOverrides.isEmpty() || sharedElementTransition == null) {
              nameOverrides.clear();
              return null;
          }
          final Fragment outFragment = fragments.firstOut;
          final ArrayMap<String, View> outSharedElements = new ArrayMap<>();
          outFragment.getView().findNamedViews(outSharedElements);
  
          final SharedElementCallback sharedElementCallback;
          final ArrayList<String> names;
          final BackStackRecord outTransaction = fragments.firstOutTransaction;
          if (fragments.firstOutIsPop) {
              sharedElementCallback = outFragment.getEnterTransitionCallback();
              names = outTransaction.mSharedElementTargetNames;
          } else {
              sharedElementCallback = outFragment.getExitTransitionCallback();
              names = outTransaction.mSharedElementSourceNames;
          }
  
          outSharedElements.retainAll(names);
          if (sharedElementCallback != null) {
              sharedElementCallback.onMapSharedElements(names, outSharedElements);
              for (int i = names.size() - 1; i >= 0; i--) {
                  String name = names.get(i);
                  View view = outSharedElements.get(name);
                  if (view == null) {
                      nameOverrides.remove(name);
                  } else if (!name.equals(view.getTransitionName())) {
                      String targetValue = nameOverrides.remove(name);
                      nameOverrides.put(view.getTransitionName(), targetValue);
                  }
              }
          } else {
              nameOverrides.retainAll(outSharedElements.keySet());
          }
          return outSharedElements;
      }
  
      /**
       * Finds the shared elements in the incoming fragment. It also calls
       * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
       * of the shared element mapping. {@code nameOverrides} is updated to match the
       * actual transition name of the mapped shared elements.
       *
       * @param nameOverrides A map of the shared element names from the starting fragment to
       *                      the final fragment's Views as given in
       *                      {@link FragmentTransaction#addSharedElement(View, String)}.
       * @param sharedElementTransition The shared element transition
       * @param fragments A structure holding the transitioning fragments in this container.
       * @return The mapping of shared element names to the Views in the hierarchy or null
       * if there is no shared element transition.
       */
      private static ArrayMap<String, View> captureInSharedElements(
              ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
              FragmentContainerTransition fragments) {
          Fragment inFragment = fragments.lastIn;
          final View fragmentView = inFragment.getView();
          if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {
              nameOverrides.clear();
              return null;
          }
          final ArrayMap<String, View> inSharedElements = new ArrayMap<>();
          fragmentView.findNamedViews(inSharedElements);
  
          final SharedElementCallback sharedElementCallback;
          final ArrayList<String> names;
          final BackStackRecord inTransaction = fragments.lastInTransaction;
          if (fragments.lastInIsPop) {
              sharedElementCallback = inFragment.getExitTransitionCallback();
              names = inTransaction.mSharedElementSourceNames;
          } else {
              sharedElementCallback = inFragment.getEnterTransitionCallback();
              names = inTransaction.mSharedElementTargetNames;
          }
  
          if (names != null) {
              inSharedElements.retainAll(names);
          }
          if (names != null && sharedElementCallback != null) {
              sharedElementCallback.onMapSharedElements(names, inSharedElements);
              for (int i = names.size() - 1; i >= 0; i--) {
                  String name = names.get(i);
                  View view = inSharedElements.get(name);
                  if (view == null) {
                      String key = findKeyForValue(nameOverrides, name);
                      if (key != null) {
                          nameOverrides.remove(key);
                      }
                  } else if (!name.equals(view.getTransitionName())) {
                      String key = findKeyForValue(nameOverrides, name);
                      if (key != null) {
                          nameOverrides.put(key, view.getTransitionName());
                      }
                  }
              }
          } else {
              retainValues(nameOverrides, inSharedElements);
          }
          return inSharedElements;
      }
  
      /**
       * Utility to find the String key in {@code map} that maps to {@code value}.
       */
      private static String findKeyForValue(ArrayMap<String, String> map, String value) {
          final int numElements = map.size();
          for (int i = 0; i < numElements; i++) {
              if (value.equals(map.valueAt(i))) {
                  return map.keyAt(i);
              }
          }
          return null;
      }
  
      /**
       * Returns the View in the incoming Fragment that should be used as the epicenter.
       *
       * @param inSharedElements The mapping of shared element names to Views in the
       *                         incoming fragment.
       * @param fragments A structure holding the transitioning fragments in this container.
       * @param enterTransition The transition used for the incoming Fragment's views
       * @param inIsPop Is the incoming fragment being added as a pop transaction?
       */
      private static View getInEpicenterView(ArrayMap<String, View> inSharedElements,
              FragmentContainerTransition fragments,
              Transition enterTransition, boolean inIsPop) {
          BackStackRecord inTransaction = fragments.lastInTransaction;
          if (enterTransition != null && inSharedElements != null
                  && inTransaction.mSharedElementSourceNames != null
                  && !inTransaction.mSharedElementSourceNames.isEmpty()) {
              final String targetName = inIsPop
                      ? inTransaction.mSharedElementSourceNames.get(0)
                      : inTransaction.mSharedElementTargetNames.get(0);
              return inSharedElements.get(targetName);
          }
          return null;
      }
  
      /**
       * Sets the epicenter for the exit transition.
       *
       * @param sharedElementTransition The shared element transition
       * @param exitTransition The transition for the outgoing fragment's views
       * @param outSharedElements Shared elements in the outgoing fragment
       * @param outIsPop Is the outgoing fragment being removed as a pop transaction?
       * @param outTransaction The transaction that caused the fragment to be removed.
       */
      private static void setOutEpicenter(TransitionSet sharedElementTransition,
              Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,
              BackStackRecord outTransaction) {
          if (outTransaction.mSharedElementSourceNames != null &&
                  !outTransaction.mSharedElementSourceNames.isEmpty()) {
              final String sourceName = outIsPop
                      ? outTransaction.mSharedElementTargetNames.get(0)
                      : outTransaction.mSharedElementSourceNames.get(0);
              final View outEpicenterView = outSharedElements.get(sourceName);
              setEpicenter(sharedElementTransition, outEpicenterView);
  
              if (exitTransition != null) {
                  setEpicenter(exitTransition, outEpicenterView);
              }
          }
      }
  
      /**
       * Sets a transition epicenter to the rectangle of a given View.
       */
      private static void setEpicenter(Transition transition, View view) {
          if (view != null) {
              final Rect epicenter = new Rect();
              view.getBoundsOnScreen(epicenter);
  
              transition.setEpicenterCallback(new Transition.EpicenterCallback() {
                  @Override
                  public Rect onGetEpicenter(Transition transition) {
                      return epicenter;
                  }
              });
          }
      }
  
      /**
       * A utility to retain only the mappings in {@code nameOverrides} that have a value
       * that has a key in {@code namedViews}. This is a useful equivalent to
       * {@link ArrayMap#retainAll(Collection)} for values.
       */
      private static void retainValues(ArrayMap<String, String> nameOverrides,
              ArrayMap<String, View> namedViews) {
          for (int i = nameOverrides.size() - 1; i >= 0; i--) {
              final String targetName = nameOverrides.valueAt(i);
              if (!namedViews.containsKey(targetName)) {
                  nameOverrides.removeAt(i);
              }
          }
      }
  
      /**
       * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or
       * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate
       * incoming or outgoing fragment.
       *
       * @param inFragment The incoming fragment
       * @param outFragment The outgoing fragment
       * @param isPop Is the incoming fragment part of a pop transaction?
       * @param sharedElements The shared element Views
       * @param isStart Call the start or end call on the SharedElementCallback
       */
      private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,
              boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {
          SharedElementCallback sharedElementCallback = isPop
                  ? outFragment.getEnterTransitionCallback()
                  : inFragment.getEnterTransitionCallback();
          if (sharedElementCallback != null) {
              ArrayList<View> views = new ArrayList<>();
              ArrayList<String> names = new ArrayList<>();
              final int count = sharedElements == null ? 0 : sharedElements.size();
              for (int i = 0; i < count; i++) {
                  names.add(sharedElements.keyAt(i));
                  views.add(sharedElements.valueAt(i));
              }
              if (isStart) {
                  sharedElementCallback.onSharedElementStart(names, views, null);
              } else {
                  sharedElementCallback.onSharedElementEnd(names, views, null);
              }
          }
      }
  
      /**
       * Finds all children of the shared elements and sets the wrapping TransitionSet
       * targets to point to those. It also limits transitions that have no targets to the
       * specific shared elements. This allows developers to target child views of the
       * shared elements specifically, but this doesn't happen by default.
       */
      private static void setSharedElementTargets(TransitionSet transition,
              View nonExistentView, ArrayList<View> sharedViews) {
          final List<View> views = transition.getTargets();
          views.clear();
          final int count = sharedViews.size();
          for (int i = 0; i < count; i++) {
              final View view = sharedViews.get(i);
              bfsAddViewChildren(views, view);
          }
          views.add(nonExistentView);
          sharedViews.add(nonExistentView);
          addTargets(transition, sharedViews);
      }
  
      /**
       * Uses a breadth-first scheme to add startView and all of its children to views.
       * It won't add a child if it is already in views.
       */
      private static void bfsAddViewChildren(final List<View> views, final View startView) {
          final int startIndex = views.size();
          if (containedBeforeIndex(views, startView, startIndex)) {
              return; // This child is already in the list, so all its children are also.
          }
          views.add(startView);
          for (int index = startIndex; index < views.size(); index++) {
              final View view = views.get(index);
              if (view instanceof ViewGroup) {
                  ViewGroup viewGroup = (ViewGroup) view;
                  final int childCount =  viewGroup.getChildCount();
                  for (int childIndex = 0; childIndex < childCount; childIndex++) {
                      final View child = viewGroup.getChildAt(childIndex);
                      if (!containedBeforeIndex(views, child, startIndex)) {
                          views.add(child);
                      }
                  }
              }
          }
      }
  
      /**
       * Does a linear search through views for view, limited to maxIndex.
       */
      private static boolean containedBeforeIndex(final List<View> views, final View view,
              final int maxIndex) {
          for (int i = 0; i < maxIndex; i++) {
              if (views.get(i) == view) {
                  return true;
              }
          }
          return false;
      }
  
      /**
       * After the transition has started, remove all targets that we added to the transitions
       * so that the transitions are left in a clean state.
       */
      private static void scheduleRemoveTargets(final Transition overalTransition,
              final Transition enterTransition, final ArrayList<View> enteringViews,
              final Transition exitTransition, final ArrayList<View> exitingViews,
              final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn) {
          overalTransition.addListener(new TransitionListenerAdapter() {
              @Override
              public void onTransitionStart(Transition transition) {
                  if (enterTransition != null) {
                      replaceTargets(enterTransition, enteringViews, null);
                  }
                  if (exitTransition != null) {
                      replaceTargets(exitTransition, exitingViews, null);
                  }
                  if (sharedElementTransition != null) {
                      replaceTargets(sharedElementTransition, sharedElementsIn, null);
                  }
              }
  
              @Override
              public void onTransitionEnd(Transition transition) {
                  transition.removeListener(this);
              }
          });
      }
  
      /**
       * This method removes the views from transitions that target ONLY those views and
       * replaces them with the new targets list.
       * The views list should match those added in addTargets and should contain
       * one view that is not in the view hierarchy (state.nonExistentView).
       */
      public static void replaceTargets(Transition transition, ArrayList<View> oldTargets,
              ArrayList<View> newTargets) {
          if (transition instanceof TransitionSet) {
              TransitionSet set = (TransitionSet) transition;
              int numTransitions = set.getTransitionCount();
              for (int i = 0; i < numTransitions; i++) {
                  Transition child = set.getTransitionAt(i);
                  replaceTargets(child, oldTargets, newTargets);
              }
          } else if (!hasSimpleTarget(transition)) {
              List<View> targets = transition.getTargets();
              if (targets != null && targets.size() == oldTargets.size() &&
                      targets.containsAll(oldTargets)) {
                  // We have an exact match. We must have added these earlier in addTargets
                  final int targetCount = newTargets == null ? 0 : newTargets.size();
                  for (int i = 0; i < targetCount; i++) {
                      transition.addTarget(newTargets.get(i));
                  }
                  for (int i = oldTargets.size() - 1; i >= 0; i--) {
                      transition.removeTarget(oldTargets.get(i));
                  }
              }
          }
      }
  
      /**
       * This method adds views as targets to the transition, but only if the transition
       * doesn't already have a target. It is best for views to contain one View object
       * that does not exist in the view hierarchy (state.nonExistentView) so that
       * when they are removed later, a list match will suffice to remove the targets.
       * Otherwise, if you happened to have targeted the exact views for the transition,
       * the replaceTargets call will remove them unexpectedly.
       */
      public static void addTargets(Transition transition, ArrayList<View> views) {
          if (transition == null) {
              return;
          }
          if (transition instanceof TransitionSet) {
              TransitionSet set = (TransitionSet) transition;
              int numTransitions = set.getTransitionCount();
              for (int i = 0; i < numTransitions; i++) {
                  Transition child = set.getTransitionAt(i);
                  addTargets(child, views);
              }
          } else if (!hasSimpleTarget(transition)) {
              List<View> targets = transition.getTargets();
              if (isNullOrEmpty(targets)) {
                  // We can just add the target views
                  int numViews = views.size();
                  for (int i = 0; i < numViews; i++) {
                      transition.addTarget(views.get(i));
                  }
              }
          }
      }
  
      /**
       * Returns true if there are any targets based on ID, transition or type.
       */
      private static boolean hasSimpleTarget(Transition transition) {
          return !isNullOrEmpty(transition.getTargetIds()) ||
                  !isNullOrEmpty(transition.getTargetNames()) ||
                  !isNullOrEmpty(transition.getTargetTypes());
      }
  
      /**
       * Simple utility to detect if a list is null or has no elements.
       */
      private static boolean isNullOrEmpty(List list) {
          return list == null || list.isEmpty();
      }
  
      private static ArrayList<View> configureEnteringExitingViews(Transition transition,
              Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
          ArrayList<View> viewList = null;
          if (transition != null) {
              viewList = new ArrayList<>();
              View root = fragment.getView();
              if (root != null) {
                  root.captureTransitioningViews(viewList);
              }
              if (sharedElements != null) {
                  viewList.removeAll(sharedElements);
              }
              if (!viewList.isEmpty()) {
                  viewList.add(nonExistentView);
                  addTargets(transition, viewList);
              }
          }
          return viewList;
      }
  
      /**
       * Sets the visibility of all Views in {@code views} to {@code visibility}.
       */
      private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) {
          if (views == null) {
              return;
          }
          for (int i = views.size() - 1; i >= 0; i--) {
              final View view = views.get(i);
              view.setVisibility(visibility);
          }
      }
  
      /**
       * Merges exit, shared element, and enter transitions so that they act together or
       * sequentially as defined in the fragments.
       */
      private static Transition mergeTransitions(Transition enterTransition,
              Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
              boolean isPop) {
          boolean overlap = true;
          if (enterTransition != null && exitTransition != null && inFragment != null) {
              overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
                      inFragment.getAllowEnterTransitionOverlap();
          }
  
          // Wrap the transitions. Explicit targets like in enter and exit will cause the
          // views to be targeted regardless of excluded views. If that happens, then the
          // excluded fragments views (hidden fragments) will still be in the transition.
  
          Transition transition;
          if (overlap) {
              // Regular transition -- do it all together
              TransitionSet transitionSet = new TransitionSet();
              if (enterTransition != null) {
                  transitionSet.addTransition(enterTransition);
              }
              if (exitTransition != null) {
                  transitionSet.addTransition(exitTransition);
              }
              if (sharedElementTransition != null) {
                  transitionSet.addTransition(sharedElementTransition);
              }
              transition = transitionSet;
          } else {
              // First do exit, then enter, but allow shared element transition to happen
              // during both.
              Transition staggered = null;
              if (exitTransition != null && enterTransition != null) {
                  staggered = new TransitionSet()
                          .addTransition(exitTransition)
                          .addTransition(enterTransition)
                          .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
              } else if (exitTransition != null) {
                  staggered = exitTransition;
              } else if (enterTransition != null) {
                  staggered = enterTransition;
              }
              if (sharedElementTransition != null) {
                  TransitionSet together = new TransitionSet();
                  if (staggered != null) {
                      together.addTransition(staggered);
                  }
                  together.addTransition(sharedElementTransition);
                  transition = together;
              } else {
                  transition = staggered;
              }
          }
          return transition;
      }
  
      /**
       * Finds the first removed fragment and last added fragments when going forward.
       * If none of the fragments have transitions, then both lists will be empty.
       *
       * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
       *                               and last fragments to be added. This will be modified by
       *                               this method.
       */
      public static void calculateFragments(BackStackRecord transaction,
              SparseArray<FragmentContainerTransition> transitioningFragments,
              boolean isReordered) {
          final int numOps = transaction.mOps.size();
          for (int opNum = 0; opNum < numOps; opNum++) {
              final BackStackRecord.Op op = transaction.mOps.get(opNum);
              addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
          }
      }
  
      /**
       * Finds the first removed fragment and last added fragments when popping the back stack.
       * If none of the fragments have transitions, then both lists will be empty.
       *
       * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
       *                               and last fragments to be added. This will be modified by
       *                               this method.
       */
      public static void calculatePopFragments(BackStackRecord transaction,
              SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
          if (!transaction.mManager.mContainer.onHasView()) {
              return; // nothing to see, so no transitions
          }
          final int numOps = transaction.mOps.size();
          for (int opNum = numOps - 1; opNum >= 0; opNum--) {
              final BackStackRecord.Op op = transaction.mOps.get(opNum);
              addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
          }
      }
  
      /**
       * Examines the {@code command} and may set the first out or last in fragment for the fragment's
       * container.
       *
       * @param transaction The executing transaction
       * @param op The operation being run.
       * @param transitioningFragments A structure holding the first in and last out fragments
       *                               for each fragment container.
       * @param isPop Is the operation a pop?
       * @param isReorderedTransaction True if the operations have been partially executed and the
       *                               added fragments have Views in the hierarchy or false if the
       *                               operations haven't been executed yet.
       */
      @SuppressWarnings("ReferenceEquality")
      private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
              SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
              boolean isReorderedTransaction) {
          final Fragment fragment = op.fragment;
          if (fragment == null) {
              return; // no fragment, no transition
          }
          final int containerId = fragment.mContainerId;
          if (containerId == 0) {
              return; // no container, no transition
          }
          final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
          boolean setLastIn = false;
          boolean wasRemoved = false;
          boolean setFirstOut = false;
          boolean wasAdded = false;
          switch (command) {
              case BackStackRecord.OP_SHOW:
                  if (isReorderedTransaction) {
                      setLastIn = fragment.mHiddenChanged && !fragment.mHidden &&
                              fragment.mAdded;
                  } else {
                      setLastIn = fragment.mHidden;
                  }
                  wasAdded = true;
                  break;
              case BackStackRecord.OP_ADD:
              case BackStackRecord.OP_ATTACH:
                  if (isReorderedTransaction) {
                      setLastIn = fragment.mIsNewlyAdded;
                  } else {
                      setLastIn = !fragment.mAdded && !fragment.mHidden;
                  }
                  wasAdded = true;
                  break;
              case BackStackRecord.OP_HIDE:
                  if (isReorderedTransaction) {
                      setFirstOut = fragment.mHiddenChanged && fragment.mAdded &&
                              fragment.mHidden;
                  } else {
                      setFirstOut = fragment.mAdded && !fragment.mHidden;
                  }
                  wasRemoved = true;
                  break;
              case BackStackRecord.OP_REMOVE:
              case BackStackRecord.OP_DETACH:
                  if (isReorderedTransaction) {
                      setFirstOut = !fragment.mAdded && fragment.mView != null
                              && fragment.mView.getVisibility() == View.VISIBLE
                              && fragment.mView.getTransitionAlpha() > 0;
                  } else {
                      setFirstOut = fragment.mAdded && !fragment.mHidden;
                  }
                  wasRemoved = true;
                  break;
          }
          FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
          if (setLastIn) {
              containerTransition =
                      ensureContainer(containerTransition, transitioningFragments, containerId);
              containerTransition.lastIn = fragment;
              containerTransition.lastInIsPop = isPop;
              containerTransition.lastInTransaction = transaction;
          }
          if (!isReorderedTransaction && wasAdded) {
              if (containerTransition != null && containerTransition.firstOut == fragment) {
                  containerTransition.firstOut = null;
              }
  
              /*
               * Ensure that fragments that are entering are at least at the CREATED state
               * so that they may load Transitions using TransitionInflater.
               */
              FragmentManagerImpl manager = transaction.mManager;
              if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED &&
                      manager.mHost.getContext().getApplicationInfo().targetSdkVersion >=
                              Build.VERSION_CODES.N && !transaction.mReorderingAllowed) {
                  manager.makeActive(fragment);
                  manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
              }
          }
          if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
              containerTransition =
                      ensureContainer(containerTransition, transitioningFragments, containerId);
              containerTransition.firstOut = fragment;
              containerTransition.firstOutIsPop = isPop;
              containerTransition.firstOutTransaction = transaction;
          }
  
          if (!isReorderedTransaction && wasRemoved &&
                  (containerTransition != null && containerTransition.lastIn == fragment)) {
              containerTransition.lastIn = null;
          }
      }
  
      /**
       * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
       * it returns the existing one. If not, one is created and added to the SparseArray and
       * returned.
       */
      private static FragmentContainerTransition ensureContainer(
              FragmentContainerTransition containerTransition,
              SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
          if (containerTransition == null) {
              containerTransition = new FragmentContainerTransition();
              transitioningFragments.put(containerId, containerTransition);
          }
          return containerTransition;
      }
  
      /**
       * Tracks the last fragment added and first fragment removed for fragment transitions.
       * This also tracks which fragments are changed by push or pop transactions.
       */
      public static class FragmentContainerTransition {
          /**
           * The last fragment added/attached/shown in its container
           */
          public Fragment lastIn;
  
          /**
           * true when lastIn was added during a pop transaction or false if added with a push
           */
          public boolean lastInIsPop;
  
          /**
           * The transaction that included the last in fragment
           */
          public BackStackRecord lastInTransaction;
  
          /**
           * The first fragment with a View that was removed/detached/hidden in its container.
           */
          public Fragment firstOut;
  
          /**
           * true when firstOut was removed during a pop transaction or false otherwise
           */
          public boolean firstOutIsPop;
  
          /**
           * The transaction that included the first out fragment
           */
          public BackStackRecord firstOutTransaction;
      }
  }
  