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.accessibility;
18 
19 import static android.os.Build.HW_TIMEOUT_MULTIPLIER;
20 
21 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.eq;
28 import static org.mockito.Mockito.spy;
29 import static org.mockito.Mockito.verify;
30 import static org.mockito.Mockito.when;
31 
32 import android.animation.Animator;
33 import android.animation.AnimatorListenerAdapter;
34 import android.animation.ObjectAnimator;
35 import android.animation.ValueAnimator;
36 import android.content.Context;
37 import android.content.pm.ActivityInfo;
38 import android.graphics.Rect;
39 import android.os.RemoteException;
40 import android.testing.AndroidTestingRunner;
41 import android.testing.TestableLooper;
42 import android.view.Display;
43 import android.view.IRotationWatcher;
44 import android.view.IWindowManager;
45 import android.view.SurfaceControl;
46 import android.view.SurfaceControlViewHost;
47 import android.view.View;
48 import android.view.WindowManager;
49 import android.view.accessibility.AccessibilityManager;
50 import android.view.animation.DecelerateInterpolator;
51 import android.view.animation.Interpolator;
52 import android.window.InputTransferToken;
53 
54 import androidx.annotation.NonNull;
55 import androidx.test.filters.SmallTest;
56 
57 import com.android.systemui.SysuiTestCase;
58 import com.android.systemui.res.R;
59 
60 import org.junit.After;
61 import org.junit.Before;
62 import org.junit.Test;
63 import org.junit.runner.RunWith;
64 import org.mockito.Mock;
65 import org.mockito.MockitoAnnotations;
66 
67 import java.util.concurrent.CountDownLatch;
68 import java.util.concurrent.TimeUnit;
69 import java.util.function.Supplier;
70 
71 @SmallTest
72 @TestableLooper.RunWithLooper
73 @RunWith(AndroidTestingRunner.class)
74 public class FullscreenMagnificationControllerTest extends SysuiTestCase {
75     private static final long ANIMATION_DURATION_MS = 100L;
76     private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER;
77     private static final long ANIMATION_TIMEOUT_MS =
78             5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER;
79     private FullscreenMagnificationController mFullscreenMagnificationController;
80     private SurfaceControlViewHost mSurfaceControlViewHost;
81     private ValueAnimator mShowHideBorderAnimator;
82     private SurfaceControl.Transaction mTransaction;
83     private TestableWindowManager mWindowManager;
84     @Mock
85     private IWindowManager mIWindowManager;
86 
87     @Before
setUp()88     public void setUp() {
89         MockitoAnnotations.initMocks(this);
90         getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost =
91                 spy(new SurfaceControlViewHost(mContext, mContext.getDisplay(),
92                         new InputTransferToken(), "FullscreenMagnification")));
93         Supplier<SurfaceControlViewHost> scvhSupplier = () -> mSurfaceControlViewHost;
94         final WindowManager wm = mContext.getSystemService(WindowManager.class);
95         mWindowManager = new TestableWindowManager(wm);
96         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
97 
98         mTransaction = new SurfaceControl.Transaction();
99         mShowHideBorderAnimator = spy(newNullTargetObjectAnimator());
100         mFullscreenMagnificationController = new FullscreenMagnificationController(
101                 mContext,
102                 mContext.getMainThreadHandler(),
103                 mContext.getMainExecutor(),
104                 mContext.getSystemService(AccessibilityManager.class),
105                 mContext.getSystemService(WindowManager.class),
106                 mIWindowManager,
107                 scvhSupplier,
108                 mTransaction,
109                 mShowHideBorderAnimator);
110     }
111 
112     @After
tearDown()113     public void tearDown() {
114         getInstrumentation().runOnMainSync(
115                 () -> mFullscreenMagnificationController
116                         .onFullscreenMagnificationActivationChanged(false));
117     }
118 
119     @Test
enableFullscreenMagnification_visibleBorder()120     public void enableFullscreenMagnification_visibleBorder()
121             throws InterruptedException, RemoteException {
122         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
123         CountDownLatch animationEndLatch = new CountDownLatch(1);
124         mTransaction.addTransactionCommittedListener(
125                 Runnable::run, transactionCommittedLatch::countDown);
126         mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
127             @Override
128             public void onAnimationEnd(Animator animation) {
129                 animationEndLatch.countDown();
130             }
131         });
132         getInstrumentation().runOnMainSync(() ->
133                 //Enable fullscreen magnification
134                 mFullscreenMagnificationController
135                         .onFullscreenMagnificationActivationChanged(true));
136         assertWithMessage("Failed to wait for transaction committed")
137                 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
138                 .isTrue();
139         assertWithMessage("Failed to wait for animation to be finished")
140                 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
141                 .isTrue();
142         verify(mShowHideBorderAnimator).start();
143         verify(mIWindowManager)
144                 .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY));
145         assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue();
146     }
147 
148     @Test
disableFullscreenMagnification_reverseAnimationAndReleaseScvh()149     public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh()
150             throws InterruptedException, RemoteException {
151         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
152         CountDownLatch enableAnimationEndLatch = new CountDownLatch(1);
153         CountDownLatch disableAnimationEndLatch = new CountDownLatch(1);
154         mTransaction.addTransactionCommittedListener(
155                 Runnable::run, transactionCommittedLatch::countDown);
156         mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
157             @Override
158             public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
159                 if (isReverse) {
160                     disableAnimationEndLatch.countDown();
161                 } else {
162                     enableAnimationEndLatch.countDown();
163                 }
164             }
165         });
166         getInstrumentation().runOnMainSync(() ->
167                 //Enable fullscreen magnification
168                 mFullscreenMagnificationController
169                         .onFullscreenMagnificationActivationChanged(true));
170         assertWithMessage("Failed to wait for transaction committed")
171                 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
172                 .isTrue();
173         assertWithMessage("Failed to wait for enabling animation to be finished")
174                 .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
175                 .isTrue();
176         verify(mShowHideBorderAnimator).start();
177 
178         getInstrumentation().runOnMainSync(() ->
179                 // Disable fullscreen magnification
180                 mFullscreenMagnificationController
181                         .onFullscreenMagnificationActivationChanged(false));
182 
183         assertWithMessage("Failed to wait for disabling animation to be finished")
184                 .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
185                 .isTrue();
186         verify(mShowHideBorderAnimator).reverse();
187         verify(mSurfaceControlViewHost).release();
188         verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class));
189     }
190 
191     @Test
onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator()192     public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator()
193             throws InterruptedException {
194         // Simulate the hiding border animation is running
195         when(mShowHideBorderAnimator.isRunning()).thenReturn(true);
196         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
197         CountDownLatch animationEndLatch = new CountDownLatch(1);
198         mTransaction.addTransactionCommittedListener(
199                 Runnable::run, transactionCommittedLatch::countDown);
200         mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
201             @Override
202             public void onAnimationEnd(Animator animation) {
203                 animationEndLatch.countDown();
204             }
205         });
206 
207         getInstrumentation().runOnMainSync(
208                 () -> mFullscreenMagnificationController
209                             .onFullscreenMagnificationActivationChanged(true));
210 
211         assertWithMessage("Failed to wait for transaction committed")
212                 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
213                 .isTrue();
214         assertWithMessage("Failed to wait for animation to be finished")
215                 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
216                         .isTrue();
217         verify(mShowHideBorderAnimator).reverse();
218     }
219 
220     @Test
onScreenSizeChanged_activated_borderChangedToExpectedSize()221     public void onScreenSizeChanged_activated_borderChangedToExpectedSize()
222             throws InterruptedException {
223         CountDownLatch transactionCommittedLatch = new CountDownLatch(1);
224         CountDownLatch animationEndLatch = new CountDownLatch(1);
225         mTransaction.addTransactionCommittedListener(
226                 Runnable::run, transactionCommittedLatch::countDown);
227         mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() {
228             @Override
229             public void onAnimationEnd(Animator animation) {
230                 animationEndLatch.countDown();
231             }
232         });
233         getInstrumentation().runOnMainSync(() ->
234                 //Enable fullscreen magnification
235                 mFullscreenMagnificationController
236                         .onFullscreenMagnificationActivationChanged(true));
237         assertWithMessage("Failed to wait for transaction committed")
238                 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS))
239                 .isTrue();
240         assertWithMessage("Failed to wait for animation to be finished")
241                 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS))
242                 .isTrue();
243         final Rect testWindowBounds = new Rect(
244                 mWindowManager.getCurrentWindowMetrics().getBounds());
245         testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
246                 testWindowBounds.right + 100, testWindowBounds.bottom + 100);
247         mWindowManager.setWindowBounds(testWindowBounds);
248 
249         getInstrumentation().runOnMainSync(() ->
250                 mFullscreenMagnificationController.onConfigurationChanged(
251                         ActivityInfo.CONFIG_SCREEN_SIZE));
252 
253         int borderOffset = mContext.getResources().getDimensionPixelSize(
254                 R.dimen.magnifier_border_width_fullscreen_with_offset)
255                 - mContext.getResources().getDimensionPixelSize(
256                 R.dimen.magnifier_border_width_fullscreen);
257         final int newWidth = testWindowBounds.width() + 2 * borderOffset;
258         final int newHeight = testWindowBounds.height() + 2 * borderOffset;
259         verify(mSurfaceControlViewHost).relayout(newWidth, newHeight);
260     }
261 
newNullTargetObjectAnimator()262     private ValueAnimator newNullTargetObjectAnimator() {
263         final ValueAnimator animator =
264                 ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f);
265         Interpolator interpolator = new DecelerateInterpolator(2.5f);
266         animator.setInterpolator(interpolator);
267         animator.setDuration(ANIMATION_DURATION_MS);
268         return animator;
269     }
270 }
271