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