1 /* 2 * Copyright (C) 2024 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 17 package com.android.systemui.animation; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.annotation.Nullable; 22 import android.app.Instrumentation; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.os.Binder; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.util.ArrayMap; 29 import android.view.SurfaceControl; 30 import android.view.WindowManager; 31 import android.window.IRemoteTransition; 32 import android.window.IRemoteTransitionFinishedCallback; 33 import android.window.RemoteTransition; 34 import android.window.TransitionInfo; 35 import android.window.WindowAnimationState; 36 import android.window.WindowContainerTransaction; 37 38 import androidx.test.filters.SmallTest; 39 import androidx.test.platform.app.InstrumentationRegistry; 40 41 import com.android.systemui.animation.shared.IOriginTransitions; 42 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 import org.junit.runners.JUnit4; 47 48 import java.util.Map; 49 import java.util.function.Predicate; 50 51 /** Unit tests for {@link OriginTransitionSession}. */ 52 @SmallTest 53 @RunWith(JUnit4.class) 54 public final class OriginTransitionSessionTest { 55 private static final ComponentName TEST_ACTIVITY_1 = new ComponentName("test", "Activity1"); 56 private static final ComponentName TEST_ACTIVITY_2 = new ComponentName("test", "Activity2"); 57 private static final ComponentName TEST_ACTIVITY_3 = new ComponentName("test", "Activity3"); 58 59 private FakeIOriginTransitions mIOriginTransitions; 60 private Instrumentation mInstrumentation; 61 private FakeIntentStarter mIntentStarter; 62 private Context mContext; 63 64 @Before setUp()65 public void setUp() { 66 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 67 mContext = mInstrumentation.getTargetContext(); 68 mIOriginTransitions = new FakeIOriginTransitions(); 69 mIntentStarter = new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2); 70 } 71 72 @Test sessionStart_withEntryAndExitTransition_transitionsPlayed()73 public void sessionStart_withEntryAndExitTransition_transitionsPlayed() { 74 FakeRemoteTransition entry = new FakeRemoteTransition(); 75 FakeRemoteTransition exit = new FakeRemoteTransition(); 76 OriginTransitionSession session = 77 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 78 .withIntentStarter(mIntentStarter) 79 .withEntryTransition(entry) 80 .withExitTransition(exit) 81 .build(); 82 83 session.start(); 84 85 assertThat(mIntentStarter.hasLaunched()).isTrue(); 86 assertThat(entry.started()).isTrue(); 87 88 runReturnTransition(mIntentStarter); 89 90 assertThat(exit.started()).isTrue(); 91 } 92 93 @Test sessionStart_withEntryTransition_transitionPlayed()94 public void sessionStart_withEntryTransition_transitionPlayed() { 95 FakeRemoteTransition entry = new FakeRemoteTransition(); 96 OriginTransitionSession session = 97 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 98 .withIntentStarter(mIntentStarter) 99 .withEntryTransition(entry) 100 .build(); 101 102 session.start(); 103 104 assertThat(mIntentStarter.hasLaunched()).isTrue(); 105 assertThat(entry.started()).isTrue(); 106 assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse(); 107 } 108 109 @Test sessionStart_withoutTransition_launchedIntent()110 public void sessionStart_withoutTransition_launchedIntent() { 111 OriginTransitionSession session = 112 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 113 .withIntentStarter(mIntentStarter) 114 .build(); 115 116 session.start(); 117 118 assertThat(mIntentStarter.hasLaunched()).isTrue(); 119 assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse(); 120 } 121 122 @Test sessionStart_cancelledByIntentStarter_transitionNotPlayed()123 public void sessionStart_cancelledByIntentStarter_transitionNotPlayed() { 124 FakeRemoteTransition entry = new FakeRemoteTransition(); 125 FakeRemoteTransition exit = new FakeRemoteTransition(); 126 mIntentStarter = 127 new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2, /* result= */ false); 128 OriginTransitionSession session = 129 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 130 .withIntentStarter(mIntentStarter) 131 .withEntryTransition(entry) 132 .withExitTransition(exit) 133 .build(); 134 135 session.start(); 136 137 assertThat(mIntentStarter.hasLaunched()).isFalse(); 138 assertThat(entry.started()).isFalse(); 139 assertThat(exit.started()).isFalse(); 140 assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse(); 141 } 142 143 @Test sessionStart_alreadyStarted_noOp()144 public void sessionStart_alreadyStarted_noOp() { 145 FakeRemoteTransition entry = new FakeRemoteTransition(); 146 FakeRemoteTransition exit = new FakeRemoteTransition(); 147 OriginTransitionSession session = 148 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 149 .withIntentStarter(mIntentStarter) 150 .withEntryTransition(entry) 151 .withExitTransition(exit) 152 .build(); 153 session.start(); 154 entry.reset(); 155 mIntentStarter.reset(); 156 157 session.start(); 158 159 assertThat(mIntentStarter.hasLaunched()).isFalse(); 160 assertThat(entry.started()).isFalse(); 161 } 162 163 @Test sessionStart_alreadyCancelled_noOp()164 public void sessionStart_alreadyCancelled_noOp() { 165 FakeRemoteTransition entry = new FakeRemoteTransition(); 166 FakeRemoteTransition exit = new FakeRemoteTransition(); 167 OriginTransitionSession session = 168 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 169 .withIntentStarter(mIntentStarter) 170 .withEntryTransition(entry) 171 .withExitTransition(exit) 172 .build(); 173 session.cancel(); 174 175 session.start(); 176 177 assertThat(mIntentStarter.hasLaunched()).isFalse(); 178 assertThat(entry.started()).isFalse(); 179 assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse(); 180 } 181 182 @Test sessionCancelled_returnTransitionNotPlayed()183 public void sessionCancelled_returnTransitionNotPlayed() { 184 FakeRemoteTransition entry = new FakeRemoteTransition(); 185 FakeRemoteTransition exit = new FakeRemoteTransition(); 186 OriginTransitionSession session = 187 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 188 .withIntentStarter(mIntentStarter) 189 .withEntryTransition(entry) 190 .withExitTransition(exit) 191 .build(); 192 193 session.start(); 194 session.cancel(); 195 196 assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse(); 197 } 198 199 @Test multipleSessionsStarted_allTransitionsPlayed()200 public void multipleSessionsStarted_allTransitionsPlayed() { 201 FakeRemoteTransition entry1 = new FakeRemoteTransition(); 202 FakeRemoteTransition exit1 = new FakeRemoteTransition(); 203 FakeIntentStarter starter1 = mIntentStarter; 204 OriginTransitionSession session1 = 205 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 206 .withIntentStarter(starter1) 207 .withEntryTransition(entry1) 208 .withExitTransition(exit1) 209 .build(); 210 FakeRemoteTransition entry2 = new FakeRemoteTransition(); 211 FakeRemoteTransition exit2 = new FakeRemoteTransition(); 212 FakeIntentStarter starter2 = new FakeIntentStarter(TEST_ACTIVITY_2, TEST_ACTIVITY_3); 213 OriginTransitionSession session2 = 214 new OriginTransitionSession.Builder(mContext, mIOriginTransitions) 215 .withIntentStarter(starter2) 216 .withEntryTransition(entry2) 217 .withExitTransition(exit2) 218 .build(); 219 220 session1.start(); 221 222 assertThat(starter1.hasLaunched()).isTrue(); 223 assertThat(entry1.started()).isTrue(); 224 225 session2.start(); 226 227 assertThat(starter2.hasLaunched()).isTrue(); 228 assertThat(entry2.started()).isTrue(); 229 230 runReturnTransition(starter2); 231 232 assertThat(exit2.started()).isTrue(); 233 234 runReturnTransition(starter1); 235 236 assertThat(exit1.started()).isTrue(); 237 } 238 runReturnTransition(FakeIntentStarter intentStarter)239 private void runReturnTransition(FakeIntentStarter intentStarter) { 240 TransitionInfo info = 241 buildTransitionInfo(intentStarter.getToActivity(), intentStarter.getFromActivity()); 242 mIOriginTransitions.runReturnTransition(intentStarter.getTransitionOfLastLaunch(), info); 243 } 244 buildTransitionInfo(ComponentName from, ComponentName to)245 private static TransitionInfo buildTransitionInfo(ComponentName from, ComponentName to) { 246 TransitionInfo info = new TransitionInfo(WindowManager.TRANSIT_OPEN, /* flags= */ 0); 247 TransitionInfo.Change c1 = 248 new TransitionInfo.Change(/* container= */ null, /* leash= */ null); 249 c1.setMode(WindowManager.TRANSIT_OPEN); 250 c1.setActivityComponent(to); 251 TransitionInfo.Change c2 = 252 new TransitionInfo.Change(/* container= */ null, /* leash= */ null); 253 c2.setMode(WindowManager.TRANSIT_CLOSE); 254 c2.setActivityComponent(from); 255 info.addChange(c2); 256 info.addChange(c1); 257 return info; 258 } 259 260 private static class FakeIntentStarter implements Predicate<RemoteTransition> { 261 private final ComponentName mFromActivity; 262 private final ComponentName mToActivity; 263 private final boolean mResult; 264 265 @Nullable private RemoteTransition mTransition; 266 private boolean mLaunched; 267 FakeIntentStarter(ComponentName from, ComponentName to)268 FakeIntentStarter(ComponentName from, ComponentName to) { 269 this(from, to, /* result= */ true); 270 } 271 FakeIntentStarter(ComponentName from, ComponentName to, boolean result)272 FakeIntentStarter(ComponentName from, ComponentName to, boolean result) { 273 mFromActivity = from; 274 mToActivity = to; 275 mResult = result; 276 } 277 278 @Override test(RemoteTransition transition)279 public boolean test(RemoteTransition transition) { 280 if (mResult) { 281 mLaunched = true; 282 mTransition = transition; 283 if (mTransition != null) { 284 TransitionInfo info = buildTransitionInfo(mFromActivity, mToActivity); 285 try { 286 transition 287 .getRemoteTransition() 288 .startAnimation( 289 new Binder(), 290 info, 291 new SurfaceControl.Transaction(), 292 new FakeFinishCallback()); 293 } catch (RemoteException e) { 294 295 } 296 } 297 } 298 return mResult; 299 } 300 301 @Nullable getTransitionOfLastLaunch()302 public RemoteTransition getTransitionOfLastLaunch() { 303 return mTransition; 304 } 305 getFromActivity()306 public ComponentName getFromActivity() { 307 return mFromActivity; 308 } 309 getToActivity()310 public ComponentName getToActivity() { 311 return mToActivity; 312 } 313 hasLaunched()314 public boolean hasLaunched() { 315 return mLaunched; 316 } 317 reset()318 public void reset() { 319 mTransition = null; 320 mLaunched = false; 321 } 322 } 323 324 private static class FakeIOriginTransitions extends IOriginTransitions.Stub { 325 private final Map<RemoteTransition, RemoteTransition> mRecords = new ArrayMap<>(); 326 327 @Override makeOriginTransition( RemoteTransition launchTransition, RemoteTransition returnTransition)328 public RemoteTransition makeOriginTransition( 329 RemoteTransition launchTransition, RemoteTransition returnTransition) { 330 mRecords.put(launchTransition, returnTransition); 331 return launchTransition; 332 } 333 334 @Override cancelOriginTransition(RemoteTransition originTransition)335 public void cancelOriginTransition(RemoteTransition originTransition) { 336 mRecords.remove(originTransition); 337 } 338 runReturnTransition(RemoteTransition originTransition, TransitionInfo info)339 public void runReturnTransition(RemoteTransition originTransition, TransitionInfo info) { 340 RemoteTransition transition = mRecords.remove(originTransition); 341 try { 342 transition 343 .getRemoteTransition() 344 .startAnimation( 345 new Binder(), 346 info, 347 new SurfaceControl.Transaction(), 348 new FakeFinishCallback()); 349 } catch (RemoteException e) { 350 351 } 352 } 353 hasPendingReturnTransitions()354 public boolean hasPendingReturnTransitions() { 355 return !mRecords.isEmpty(); 356 } 357 } 358 359 private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub { 360 @Override onTransitionFinished( WindowContainerTransaction wct, SurfaceControl.Transaction sct)361 public void onTransitionFinished( 362 WindowContainerTransaction wct, SurfaceControl.Transaction sct) {} 363 } 364 365 private static class FakeRemoteTransition extends IRemoteTransition.Stub { 366 private boolean mStarted; 367 368 @Override startAnimation( IBinder token, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)369 public void startAnimation( 370 IBinder token, 371 TransitionInfo info, 372 SurfaceControl.Transaction t, 373 IRemoteTransitionFinishedCallback finishCallback) 374 throws RemoteException { 375 mStarted = true; 376 finishCallback.onTransitionFinished(null, null); 377 } 378 379 @Override mergeAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishCallback)380 public void mergeAnimation( 381 IBinder transition, 382 TransitionInfo info, 383 SurfaceControl.Transaction t, 384 IBinder mergeTarget, 385 IRemoteTransitionFinishedCallback finishCallback) {} 386 387 @Override takeOverAnimation( IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)388 public void takeOverAnimation( 389 IBinder transition, 390 TransitionInfo info, 391 SurfaceControl.Transaction t, 392 IRemoteTransitionFinishedCallback finishCallback, 393 WindowAnimationState[] states) {} 394 395 @Override onTransitionConsumed(IBinder transition, boolean aborted)396 public void onTransitionConsumed(IBinder transition, boolean aborted) {} 397 started()398 public boolean started() { 399 return mStarted; 400 } 401 reset()402 public void reset() { 403 mStarted = false; 404 } 405 } 406 } 407