1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.transition.cts; 17 18 import static com.android.compatibility.common.util.CtsMockitoUtils.within; 19 20 import static junit.framework.Assert.assertFalse; 21 import static junit.framework.Assert.assertTrue; 22 import static junit.framework.Assert.fail; 23 24 import static org.junit.Assert.assertEquals; 25 import static org.mockito.Matchers.any; 26 import static org.mockito.Mockito.mock; 27 import static org.mockito.Mockito.times; 28 import static org.mockito.Mockito.verify; 29 30 import android.app.ActivityOptions; 31 import android.app.SharedElementCallback; 32 import android.content.Intent; 33 import android.graphics.Bitmap; 34 import android.graphics.drawable.BitmapDrawable; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.support.test.filters.MediumTest; 38 import android.support.test.runner.AndroidJUnit4; 39 import android.transition.Fade; 40 import android.transition.Transition; 41 import android.transition.Transition.TransitionListener; 42 import android.transition.TransitionListenerAdapter; 43 import android.view.View; 44 import android.view.ViewGroup; 45 46 import com.android.compatibility.common.util.PollingCheck; 47 import com.android.compatibility.common.util.transition.TargetTracking; 48 import com.android.compatibility.common.util.transition.TrackingTransition; 49 import com.android.compatibility.common.util.transition.TrackingVisibility; 50 51 import org.junit.After; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 55 import java.util.List; 56 import java.util.Set; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.concurrent.TimeUnit; 59 import java.util.stream.Collectors; 60 61 @MediumTest 62 @RunWith(AndroidJUnit4.class) 63 public class ActivityTransitionTest extends BaseTransitionTest { 64 private TransitionListener mExitListener; 65 private TransitionListener mReenterListener; 66 private TransitionListener mSharedElementReenterListener; 67 private TrackingVisibility mExitTransition; 68 private TrackingVisibility mReenterTransition; 69 private TrackingTransition mSharedElementReenterTransition; 70 71 @Override 72 public void setup() { 73 super.setup(); 74 setTransitions(new TrackingVisibility(), new TrackingVisibility(), 75 new TrackingTransition()); 76 } 77 78 private void setTransitions(TrackingVisibility exit, TrackingVisibility reenter, 79 TrackingTransition sharedElementReenter) { 80 mExitTransition = exit; 81 mExitListener = mock(TransitionListener.class); 82 mExitTransition.addListener(mExitListener); 83 mActivity.getWindow().setExitTransition(mExitTransition); 84 85 mReenterTransition = reenter; 86 mReenterListener = mock(TransitionListener.class); 87 mReenterTransition.addListener(mReenterListener); 88 mActivity.getWindow().setReenterTransition(mReenterTransition); 89 90 mSharedElementReenterTransition = sharedElementReenter; 91 mSharedElementReenterListener = mock(TransitionListener.class); 92 mSharedElementReenterTransition.addListener(mSharedElementReenterListener); 93 mActivity.getWindow().setSharedElementReenterTransition(mSharedElementReenterTransition); 94 } 95 96 @After 97 public void cleanup() throws Throwable { 98 if (TargetActivity.sLastCreated != null) { 99 mActivityRule.runOnUiThread(() -> TargetActivity.sLastCreated.finish()); 100 } 101 TargetActivity.sLastCreated = null; 102 } 103 104 // When using ActivityOptions.makeBasic(), no transitions should run 105 @Test 106 public void testMakeBasic() throws Throwable { 107 assertFalse(mActivity.isActivityTransitionRunning()); 108 mActivityRule.runOnUiThread(() -> { 109 Intent intent = new Intent(mActivity, TargetActivity.class); 110 ActivityOptions activityOptions = 111 ActivityOptions.makeBasic(); 112 mActivity.startActivity(intent, activityOptions.toBundle()); 113 }); 114 115 assertFalse(mActivity.isActivityTransitionRunning()); 116 117 TargetActivity targetActivity = waitForTargetActivity(); 118 assertFalse(targetActivity.isActivityTransitionRunning()); 119 mActivityRule.runOnUiThread(() -> { 120 targetActivity.finish(); 121 }); 122 123 assertFalse(targetActivity.isActivityTransitionRunning()); 124 assertFalse(mActivity.isActivityTransitionRunning()); 125 } 126 127 // Views that are outside the visible area only during the shared element start 128 // should not be stripped from the transition. 129 @Test 130 public void viewsNotStripped() throws Throwable { 131 enterScene(R.layout.scene10); 132 mActivityRule.runOnUiThread(() -> { 133 View sharedElement = mActivity.findViewById(R.id.blueSquare); 134 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, 135 sharedElement, "holder").toBundle(); 136 Intent intent = new Intent(mActivity, TargetActivity.class); 137 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene12); 138 mActivity.startActivity(intent, options); 139 }); 140 141 TargetActivity targetActivity = waitForTargetActivity(); 142 verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); 143 verify(mExitListener, times(1)).onTransitionEnd(any()); 144 145 // Now check the targets... they should all be there 146 assertTargetContains(targetActivity.enterTransition, 147 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 148 assertTargetExcludes(targetActivity.enterTransition, R.id.holder); 149 150 assertTargetContains(targetActivity.sharedElementEnterTransition, R.id.holder); 151 assertTargetExcludes(targetActivity.sharedElementEnterTransition, 152 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 153 154 assertTargetContains(mExitTransition, R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); 155 assertTargetExcludes(mExitTransition, R.id.blueSquare, R.id.holder); 156 157 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.redSquare).getVisibility()); 158 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.greenSquare).getVisibility()); 159 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.holder).getVisibility()); 160 161 assertEquals(1, targetActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); 162 assertEquals(1, targetActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); 163 assertEquals(1, targetActivity.findViewById(R.id.holder).getAlpha(), 0.01f); 164 165 mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); 166 verify(mReenterListener, within(3000)).onTransitionEnd(any()); 167 verify(mSharedElementReenterListener, within(3000)).onTransitionEnd(any()); 168 verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); 169 170 // return targets are stripped also 171 assertTargetContains(targetActivity.returnTransition, 172 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 173 assertTargetExcludes(targetActivity.returnTransition, R.id.holder); 174 175 assertTargetContains(mReenterTransition, 176 R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); 177 assertTargetExcludes(mReenterTransition, R.id.blueSquare, R.id.holder); 178 179 assertTargetContains(targetActivity.sharedElementReturnTransition, 180 R.id.holder); 181 assertTargetExcludes(targetActivity.sharedElementReturnTransition, 182 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 183 184 assertTargetContains(mSharedElementReenterTransition, R.id.blueSquare); 185 assertTargetExcludes(mSharedElementReenterTransition, 186 R.id.redSquare, R.id.greenSquare, R.id.yellowSquare); 187 188 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.redSquare).getVisibility()); 189 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.greenSquare).getVisibility()); 190 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.holder).getVisibility()); 191 192 assertEquals(1, mActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); 193 assertEquals(1, mActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); 194 assertEquals(1, mActivity.findViewById(R.id.holder).getAlpha(), 0.01f); 195 196 TargetActivity.sLastCreated = null; 197 } 198 199 // Views that are outside the visible area during initial layout should be stripped from 200 // the transition. 201 @Test 202 public void viewsStripped() throws Throwable { 203 enterScene(R.layout.scene13); 204 mActivityRule.runOnUiThread(() -> { 205 View sharedElement = mActivity.findViewById(R.id.redSquare); 206 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, 207 sharedElement, "redSquare").toBundle(); 208 Intent intent = new Intent(mActivity, TargetActivity.class); 209 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene13); 210 mActivity.startActivity(intent, options); 211 }); 212 213 TargetActivity targetActivity = waitForTargetActivity(); 214 verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); 215 verify(mExitListener, times(1)).onTransitionEnd(any()); 216 217 // Now check the targets... they should all be stripped 218 assertTargetExcludes(targetActivity.enterTransition, R.id.holder, 219 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 220 221 assertTargetExcludes(mExitTransition, R.id.holder, 222 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 223 224 assertTargetContains(targetActivity.sharedElementEnterTransition, R.id.redSquare); 225 assertTargetExcludes(targetActivity.sharedElementEnterTransition, 226 R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 227 228 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.redSquare).getVisibility()); 229 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.greenSquare).getVisibility()); 230 assertEquals(View.VISIBLE, targetActivity.findViewById(R.id.holder).getVisibility()); 231 232 assertEquals(1, targetActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); 233 assertEquals(1, targetActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); 234 assertEquals(1, targetActivity.findViewById(R.id.holder).getAlpha(), 0.01f); 235 236 mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); 237 verify(mReenterListener, within(3000)).onTransitionEnd(any()); 238 verify(mSharedElementReenterListener, within(3000)).onTransitionEnd(any()); 239 verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); 240 241 // return targets are stripped also 242 assertTargetExcludes(targetActivity.returnTransition, 243 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 244 245 assertTargetExcludes(mReenterTransition, R.id.holder, 246 R.id.redSquare, R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 247 248 assertTargetContains(targetActivity.sharedElementReturnTransition, 249 R.id.redSquare); 250 assertTargetExcludes(targetActivity.sharedElementReturnTransition, 251 R.id.greenSquare, R.id.blueSquare, R.id.yellowSquare); 252 253 assertTargetContains(mSharedElementReenterTransition, R.id.redSquare); 254 assertTargetExcludes(mSharedElementReenterTransition, 255 R.id.blueSquare, R.id.greenSquare, R.id.yellowSquare); 256 257 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.greenSquare).getVisibility()); 258 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.holder).getVisibility()); 259 assertEquals(View.VISIBLE, mActivity.findViewById(R.id.redSquare).getVisibility()); 260 261 assertEquals(1, mActivity.findViewById(R.id.redSquare).getAlpha(), 0.01f); 262 assertEquals(1, mActivity.findViewById(R.id.greenSquare).getAlpha(), 0.01f); 263 assertEquals(1, mActivity.findViewById(R.id.holder).getAlpha(), 0.01f); 264 265 TargetActivity.sLastCreated = null; 266 } 267 268 // When an exit transition takes longer than it takes the activity to cover it (and onStop 269 // is called), the exiting views should become visible. 270 @Test 271 public void earlyExitStop() throws Throwable { 272 enterScene(R.layout.scene1); 273 final View hello = mActivity.findViewById(R.id.hello); 274 final View red = mActivity.findViewById(R.id.redSquare); 275 final View green = mActivity.findViewById(R.id.greenSquare); 276 mActivityRule.runOnUiThread(() -> { 277 Fade fade = new Fade(); 278 fade.setDuration(10000); 279 fade.addListener(mExitListener); 280 mActivity.getWindow().setExitTransition(fade); 281 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity).toBundle(); 282 Intent intent = new Intent(mActivity, TargetActivity.class); 283 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene4); 284 mActivity.startActivity(intent, options); 285 }); 286 287 TargetActivity targetActivity = waitForTargetActivity(); 288 verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); 289 verify(mExitListener, within(3000)).onTransitionEnd(any()); 290 291 mActivityRule.runOnUiThread(() -> { 292 // Verify that the exited views have an alpha of 1 and are visible 293 assertEquals(1.0f, hello.getAlpha(), 0.01f); 294 assertEquals(1.0f, red.getAlpha(), 0.01f); 295 assertEquals(1.0f, green.getAlpha(), 0.01f); 296 297 assertEquals(View.VISIBLE, hello.getVisibility()); 298 assertEquals(View.VISIBLE, red.getVisibility()); 299 assertEquals(View.VISIBLE, green.getVisibility()); 300 targetActivity.finish(); 301 }); 302 } 303 304 @Test 305 public void testAnimationQuery() throws Throwable { 306 enterScene(R.layout.scene1); 307 assertFalse(mActivity.isActivityTransitionRunning()); 308 mActivityRule.runOnUiThread(() -> { 309 mActivity.getWindow().setExitTransition(new Fade()); 310 Intent intent = new Intent(mActivity, TargetActivity.class); 311 ActivityOptions activityOptions = 312 ActivityOptions.makeSceneTransitionAnimation(mActivity); 313 mActivity.startActivity(intent, activityOptions.toBundle()); 314 }); 315 316 assertTrue(mActivity.isActivityTransitionRunning()); 317 318 TargetActivity targetActivity = waitForTargetActivity(); 319 assertTrue(targetActivity.isActivityTransitionRunning()); 320 mActivityRule.runOnUiThread(() -> { }); 321 PollingCheck.waitFor(() -> !targetActivity.isActivityTransitionRunning()); 322 323 assertFalse(mActivity.isActivityTransitionRunning()); 324 mActivityRule.runOnUiThread(() -> { 325 targetActivity.finishAfterTransition(); 326 // The target activity transition should start right away 327 assertTrue(targetActivity.isActivityTransitionRunning()); 328 }); 329 330 // The source activity transition should start sometime later 331 PollingCheck.waitFor(() -> mActivity.isActivityTransitionRunning()); 332 PollingCheck.waitFor(() -> !mActivity.isActivityTransitionRunning()); 333 } 334 335 // Views that are excluded from the exit/enter transition shouldn't change visibility 336 @Test 337 public void untargetedViews() throws Throwable { 338 enterScene(R.layout.scene10); 339 340 final View redSquare = mActivity.findViewById(R.id.redSquare); 341 342 setTransitions(new TrackingVisibilityWithAnimator(), new TrackingVisibilityWithAnimator(), 343 new TrackingTransition()); 344 TransitionListener redSquareValidator = new TransitionListenerAdapter() { 345 @Override 346 public void onTransitionStart(Transition transition) { 347 assertEquals(View.VISIBLE, redSquare.getVisibility()); 348 } 349 350 @Override 351 public void onTransitionEnd(Transition transition) { 352 assertEquals(View.VISIBLE, redSquare.getVisibility()); 353 } 354 }; 355 mExitTransition.addListener(redSquareValidator); 356 mReenterTransition.addListener(redSquareValidator); 357 358 mExitTransition.excludeTarget(R.id.redSquare, true); 359 mReenterTransition.excludeTarget(R.id.redSquare, true); 360 361 mActivity.runOnUiThread(() -> { 362 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity).toBundle(); 363 Intent intent = new Intent(mActivity, TargetActivity.class); 364 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene12); 365 intent.putExtra(TargetActivity.EXTRA_EXCLUDE_ID, R.id.redSquare); 366 intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true); 367 mActivity.startActivity(intent, options); 368 }); 369 370 verify(mExitListener, within(3000)).onTransitionEnd(any()); 371 372 TargetActivity targetActivity = waitForTargetActivity(); 373 374 assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); 375 assertEquals(View.VISIBLE, targetActivity.startVisibility); 376 assertEquals(View.VISIBLE, targetActivity.endVisibility); 377 378 // Reset so that we know that they are modified when returning 379 targetActivity.startVisibility = targetActivity.endVisibility = -1; 380 381 targetActivity.transitionComplete = new CountDownLatch(1); 382 383 mActivity.runOnUiThread(() -> { 384 targetActivity.finishAfterTransition(); 385 }); 386 387 assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); 388 assertEquals(View.VISIBLE, targetActivity.startVisibility); 389 assertEquals(View.VISIBLE, targetActivity.endVisibility); 390 391 assertTrue(targetActivity.transitionComplete.await(1, TimeUnit.SECONDS)); 392 verify(mReenterListener, within(3000)).onTransitionEnd(any()); 393 394 TargetActivity.sLastCreated = null; 395 } 396 397 // Starting a shared element transition and then removing the view shouldn't cause problems. 398 @Test 399 public void removeSharedViews() throws Throwable { 400 enterScene(R.layout.scene1); 401 402 final View redSquare = mActivity.findViewById(R.id.redSquare); 403 final ViewGroup parent = (ViewGroup) redSquare.getParent(); 404 405 mActivityRule.runOnUiThread(() -> { 406 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, 407 redSquare, "red").toBundle(); 408 Intent intent = new Intent(mActivity, TargetActivity.class); 409 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene2); 410 intent.putExtra(TargetActivity.EXTRA_USE_ANIMATOR, true); 411 parent.removeView(redSquare); 412 mActivity.startActivity(intent, options); 413 }); 414 415 416 TargetActivity targetActivity = waitForTargetActivity(); 417 verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); 418 419 mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); 420 mActivityRule.runOnUiThread(() -> parent.removeAllViews()); 421 422 verify(targetActivity.returnListener, times(1)).onTransitionEnd(any()); 423 TargetActivity.sLastCreated = null; 424 } 425 426 // Ensure that the shared element view copy is the correct image of the shared element view 427 // source 428 @Test 429 public void sharedElementCopied() throws Throwable { 430 enterScene(R.layout.scene1); 431 432 mActivityRule.runOnUiThread(() -> { 433 View sharedElement = mActivity.findViewById(R.id.redSquare); 434 Bundle options = ActivityOptions.makeSceneTransitionAnimation(mActivity, 435 sharedElement, "red").toBundle(); 436 Intent intent = new Intent(mActivity, TargetActivity.class); 437 intent.putExtra(TargetActivity.EXTRA_LAYOUT_ID, R.layout.scene2); 438 mActivity.startActivity(intent, options); 439 }); 440 441 TargetActivity targetActivity = waitForTargetActivity(); 442 verify(targetActivity.enterListener, within(3000)).onTransitionEnd(any()); 443 verify(mExitListener, times(1)).onTransitionEnd(any()); 444 445 final CountDownLatch startCalled = new CountDownLatch(1); 446 final SharedElementCallback sharedElementCallback = new SharedElementCallback() { 447 @Override 448 public void onSharedElementStart(List<String> sharedElementNames, 449 List<View> sharedElements, 450 List<View> sharedElementSnapshots) { 451 int index = sharedElementNames.indexOf("red"); 452 View sharedElement = sharedElementSnapshots.get(index); 453 Drawable backgroundDrawable = sharedElement.getBackground(); 454 BitmapDrawable bitmapDrawable = (BitmapDrawable) backgroundDrawable; 455 Bitmap bitmap = bitmapDrawable.getBitmap(); 456 Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false); 457 assertEquals(0xFFFF0000, copy.getPixel(1, 1)); 458 startCalled.countDown(); 459 super.onSharedElementStart(sharedElementNames, sharedElements, 460 sharedElementSnapshots); 461 } 462 }; 463 464 mActivity.setExitSharedElementCallback(sharedElementCallback); 465 mActivityRule.runOnUiThread(() -> targetActivity.finishAfterTransition()); 466 467 // Should only take a short time, but there's no need to rush it on failure. 468 assertTrue(startCalled.await(5, TimeUnit.SECONDS)); 469 470 TargetActivity.sLastCreated = null; 471 } 472 473 private TargetActivity waitForTargetActivity() throws Throwable { 474 PollingCheck.waitFor(() -> TargetActivity.sLastCreated != null); 475 // Just make sure that we're not in the middle of running on the UI thread. 476 mActivityRule.runOnUiThread(() -> { }); 477 return TargetActivity.sLastCreated; 478 } 479 480 private Set<Integer> getTargetViewIds(TargetTracking transition) { 481 return transition.getTrackedTargets().stream() 482 .map(v -> v.getId()) 483 .collect(Collectors.toSet()); 484 } 485 486 private void assertTargetContains(TargetTracking transition, int... ids) { 487 Set<Integer> targets = getTargetViewIds(transition); 488 for (int id : ids) { 489 assertTrueWithId(id, "%s was not included from the transition", targets.contains(id)); 490 } 491 } 492 493 private void assertTargetExcludes(TargetTracking transition, int... ids) { 494 Set<Integer> targets = getTargetViewIds(transition); 495 for (int id : ids) { 496 assertTrueWithId(id, "%s was not excluded from the transition", !targets.contains(id)); 497 } 498 } 499 500 private void assertTrueWithId(int id, String message, boolean valueToAssert) { 501 if (!valueToAssert) { 502 fail(String.format(message, mActivity.getResources().getResourceName(id))); 503 } 504 } 505 } 506