Merge "Backport endTransitions" into oc-support-26.0-dev
diff --git a/api/26.0.0-SNAPSHOT.txt b/api/26.0.0-SNAPSHOT.txt
index 360dc1d..a38526f 100644
--- a/api/26.0.0-SNAPSHOT.txt
+++ b/api/26.0.0-SNAPSHOT.txt
@@ -2206,6 +2206,7 @@
     ctor public TransitionManager();
     method public static void beginDelayedTransition(android.view.ViewGroup);
     method public static void beginDelayedTransition(android.view.ViewGroup, android.support.transition.Transition);
+    method public static void endTransitions(android.view.ViewGroup);
     method public static void go(android.support.transition.Scene);
     method public static void go(android.support.transition.Scene, android.support.transition.Transition);
     method public void setTransition(android.support.transition.Scene, android.support.transition.Transition);
diff --git a/transition/src/android/support/transition/Transition.java b/transition/src/android/support/transition/Transition.java
index 369a3e3..8857135 100644
--- a/transition/src/android/support/transition/Transition.java
+++ b/transition/src/android/support/transition/Transition.java
@@ -1963,6 +1963,27 @@
     }
 
     /**
+     * Force the transition to move to its end state, ending all the animators.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    void forceToEnd(ViewGroup sceneRoot) {
+        ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
+        int numOldAnims = runningAnimators.size();
+        if (sceneRoot != null) {
+            WindowIdImpl windowId = ViewUtils.getWindowId(sceneRoot);
+            for (int i = numOldAnims - 1; i >= 0; i--) {
+                AnimationInfo info = runningAnimators.valueAt(i);
+                if (info.mView != null && windowId != null && windowId.equals(info.mWindowId)) {
+                    Animator anim = runningAnimators.keyAt(i);
+                    anim.end();
+                }
+            }
+        }
+    }
+
+    /**
      * This method cancels a transition that is currently running.
      *
      * @hide
diff --git a/transition/src/android/support/transition/TransitionManager.java b/transition/src/android/support/transition/TransitionManager.java
index 105aca4..f65a464 100644
--- a/transition/src/android/support/transition/TransitionManager.java
+++ b/transition/src/android/support/transition/TransitionManager.java
@@ -409,4 +409,22 @@
         }
     }
 
+    /**
+     * Ends all pending and ongoing transitions on the specified scene root.
+     *
+     * @param sceneRoot The root of the View hierarchy to end transitions on.
+     */
+    public static void endTransitions(final ViewGroup sceneRoot) {
+        sPendingTransitions.remove(sceneRoot);
+        final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
+        if (runningTransitions != null && !runningTransitions.isEmpty()) {
+            // Make a copy in case this is called by an onTransitionEnd listener
+            ArrayList<Transition> copy = new ArrayList<>(runningTransitions);
+            for (int i = copy.size() - 1; i >= 0; i--) {
+                final Transition transition = copy.get(i);
+                transition.forceToEnd(sceneRoot);
+            }
+        }
+    }
+
 }
diff --git a/transition/src/android/support/transition/TransitionSet.java b/transition/src/android/support/transition/TransitionSet.java
index df63414..404245a 100644
--- a/transition/src/android/support/transition/TransitionSet.java
+++ b/transition/src/android/support/transition/TransitionSet.java
@@ -530,6 +530,17 @@
         }
     }
 
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @Override
+    void forceToEnd(ViewGroup sceneRoot) {
+        super.forceToEnd(sceneRoot);
+        int numTransitions = mTransitions.size();
+        for (int i = 0; i < numTransitions; ++i) {
+            mTransitions.get(i).forceToEnd(sceneRoot);
+        }
+    }
+
     @Override
     TransitionSet setSceneRoot(ViewGroup sceneRoot) {
         super.setSceneRoot(sceneRoot);
diff --git a/transition/tests/src/android/support/transition/TransitionManagerTest.java b/transition/tests/src/android/support/transition/TransitionManagerTest.java
index 7f49982..dc4f983 100644
--- a/transition/tests/src/android/support/transition/TransitionManagerTest.java
+++ b/transition/tests/src/android/support/transition/TransitionManagerTest.java
@@ -20,6 +20,11 @@
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
 
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.MediumTest;
@@ -132,4 +137,47 @@
         });
     }
 
+    @Test
+    public void testEndTransitions() throws Throwable {
+        final ViewGroup root = rule.getActivity().getRoot();
+        final Transition transition = new AutoTransition();
+        // This transition is very long, but will be forced to end as soon as it starts
+        transition.setDuration(30000);
+        final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+        transition.addListener(listener);
+        rule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TransitionManager.go(mScenes[0], transition);
+            }
+        });
+        verify(listener, timeout(3000)).onTransitionStart(any(Transition.class));
+        rule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TransitionManager.endTransitions(root);
+            }
+        });
+        verify(listener, timeout(3000)).onTransitionEnd(any(Transition.class));
+    }
+
+    @Test
+    public void testEndTransitionsBeforeStarted() throws Throwable {
+        final ViewGroup root = rule.getActivity().getRoot();
+        final Transition transition = new AutoTransition();
+        transition.setDuration(0);
+        final Transition.TransitionListener listener = mock(Transition.TransitionListener.class);
+        transition.addListener(listener);
+        rule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TransitionManager.go(mScenes[0], transition);
+                // This terminates the transition before it starts
+                TransitionManager.endTransitions(root);
+            }
+        });
+        verify(listener, never()).onTransitionStart(any(Transition.class));
+        verify(listener, never()).onTransitionEnd(any(Transition.class));
+    }
+
 }